| Files: | 76 |
|---|---|
| Lines: | 14056 |
| Covered: | 2579 / 4148 (62.2%) |
| Lines covered: | 67 / 116 (57.8%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Interfaces/IActivePool.sol"; |
||
| 6 |
import "./Interfaces/ICollSurplusPool.sol"; |
||
| 7 |
import "./Dependencies/ICollateralToken.sol"; |
||
| 8 |
import "./Interfaces/ICdpManagerData.sol"; |
||
| 9 |
import "./Dependencies/ERC3156FlashLender.sol"; |
||
| 10 |
import "./Dependencies/SafeERC20.sol"; |
||
| 11 |
import "./Dependencies/ReentrancyGuard.sol"; |
||
| 12 |
import "./Dependencies/AuthNoOwner.sol"; |
||
| 13 |
import "./Dependencies/BaseMath.sol"; |
||
| 14 | |||
| 15 |
/** |
||
| 16 |
* The Active Pool holds the collateral and EBTC debt (but not EBTC tokens) for all active cdps. |
||
| 17 |
* |
||
| 18 |
* When a cdp is liquidated, it's collateral and EBTC debt are transferred from the Active Pool, to either the |
||
| 19 |
* Stability Pool, the Default Pool, or both, depending on the liquidation conditions. |
||
| 20 |
*/ |
||
| 21 |
contract ActivePool is IActivePool, ERC3156FlashLender, ReentrancyGuard, BaseMath, AuthNoOwner {
|
||
| 22 |
using SafeERC20 for IERC20; |
||
| 23 |
string public constant NAME = "ActivePool"; |
||
| 24 | |||
| 25 |
address public immutable borrowerOperationsAddress; |
||
| 26 |
address public immutable cdpManagerAddress; |
||
| 27 |
address public immutable collSurplusPoolAddress; |
||
| 28 |
√
|
⟳
|
address public feeRecipientAddress; |
| 29 | |||
| 30 |
uint256 internal systemCollShares; // deposited collateral tracker |
||
| 31 |
uint256 internal systemDebt; |
||
| 32 |
uint256 internal feeRecipientCollShares; // coll shares claimable by fee recipient |
||
| 33 |
ICollateralToken public collateral; |
||
| 34 | |||
| 35 |
// --- Contract setters --- |
||
| 36 | |||
| 37 |
/// @notice Constructor for the ActivePool contract |
||
| 38 |
/// @dev Initializes the contract with the borrowerOperationsAddress, cdpManagerAddress, collateral token address, collSurplusAddress, and feeRecipientAddress |
||
| 39 |
/// @param _borrowerOperationsAddress The address of the Borrower Operations contract |
||
| 40 |
/// @param _cdpManagerAddress The address of the Cdp Manager contract |
||
| 41 |
/// @param _collTokenAddress The address of the collateral token |
||
| 42 |
/// @param _collSurplusAddress The address of the collateral surplus pool |
||
| 43 |
/// @param _feeRecipientAddress The address of the fee recipient |
||
| 44 | |||
| 45 |
constructor( |
||
| 46 |
address _borrowerOperationsAddress, |
||
| 47 |
address _cdpManagerAddress, |
||
| 48 |
address _collTokenAddress, |
||
| 49 |
address _collSurplusAddress, |
||
| 50 |
address _feeRecipientAddress |
||
| 51 |
) {
|
||
| 52 |
√
|
borrowerOperationsAddress = _borrowerOperationsAddress; |
|
| 53 |
√
|
cdpManagerAddress = _cdpManagerAddress; |
|
| 54 |
√
|
collateral = ICollateralToken(_collTokenAddress); |
|
| 55 |
√
|
collSurplusPoolAddress = _collSurplusAddress; |
|
| 56 |
√
|
feeRecipientAddress = _feeRecipientAddress; |
|
| 57 | |||
| 58 |
// TEMP: read authority to avoid signature change |
||
| 59 |
√
|
address _authorityAddress = address(AuthNoOwner(cdpManagerAddress).authority()); |
|
| 60 |
√
|
if (_authorityAddress != address(0)) {
|
|
| 61 |
√
|
_initializeAuthority(_authorityAddress); |
|
| 62 |
} |
||
| 63 | |||
| 64 |
√
|
emit FeeRecipientAddressChanged(_feeRecipientAddress); |
|
| 65 |
} |
||
| 66 | |||
| 67 |
// --- Getters for public variables. Required by IPool interface --- |
||
| 68 | |||
| 69 |
/// @notice Amount of stETH collateral shares in the contract |
||
| 70 |
/// @dev Not necessarily equal to the the contract's raw systemCollShares balance - tokens can be forcibly sent to contracts |
||
| 71 |
/// @return uint256 The amount of systemCollShares allocated to the pool |
||
| 72 | |||
| 73 |
√
|
⟳
|
function getSystemCollShares() external view override returns (uint256) {
|
| 74 |
√
|
⟳
|
return systemCollShares; |
| 75 |
} |
||
| 76 | |||
| 77 |
/// @notice Returns the systemDebt state variable |
||
| 78 |
/// @dev The amount of EBTC debt in the pool. Like systemCollShares, this is not necessarily equal to the contract's EBTC token balance - tokens can be forcibly sent to contracts |
||
| 79 |
/// @return uint256 The amount of EBTC debt in the pool |
||
| 80 | |||
| 81 |
√
|
⟳
|
function getSystemDebt() external view override returns (uint256) {
|
| 82 |
√
|
⟳
|
return systemDebt; |
| 83 |
} |
||
| 84 | |||
| 85 |
/// @notice The amount of stETH collateral shares claimable by the fee recipient |
||
| 86 |
/// @return uint256 The amount of collateral shares claimable by the fee recipient |
||
| 87 | |||
| 88 |
√
|
⟳
|
function getFeeRecipientClaimableCollShares() external view override returns (uint256) {
|
| 89 |
√
|
⟳
|
return feeRecipientCollShares; |
| 90 |
} |
||
| 91 | |||
| 92 |
// --- Pool functionality --- |
||
| 93 | |||
| 94 |
/// @notice Sends stETH collateral shares to a specified account |
||
| 95 |
/// @dev Only for use by system contracts, the caller must be either BorrowerOperations or CdpManager |
||
| 96 |
/// @param _account The address of the account to send stETH to |
||
| 97 |
/// @param _shares The amount of stETH shares to send |
||
| 98 | |||
| 99 |
function transferSystemCollShares(address _account, uint256 _shares) public override {
|
||
| 100 |
√
|
⟳
|
_requireCallerIsBOorCdpM(); |
| 101 | |||
| 102 |
√
|
⟳
|
uint256 cachedSystemCollShares = systemCollShares; |
| 103 |
√
|
⟳
|
require(cachedSystemCollShares >= _shares, "!ActivePoolBal"); |
| 104 |
unchecked {
|
||
| 105 |
// Can use unchecked due to above |
||
| 106 |
√
|
⟳
|
cachedSystemCollShares -= _shares; // Updating here avoids an SLOAD |
| 107 |
} |
||
| 108 | |||
| 109 |
√
|
⟳
|
systemCollShares = cachedSystemCollShares; |
| 110 | |||
| 111 |
√
|
⟳
|
emit SystemCollSharesUpdated(cachedSystemCollShares); |
| 112 |
√
|
⟳
|
emit CollSharesTransferred(_account, _shares); |
| 113 | |||
| 114 |
√
|
⟳
|
_transferCollSharesWithContractHooks(_account, _shares); |
| 115 |
} |
||
| 116 | |||
| 117 |
/// @notice Sends stETH to a specified account, drawing from both core shares and liquidator rewards shares |
||
| 118 |
/// @notice Liquidator reward shares are not tracked via internal accounting in the active pool and are assumed to be present in expected amount as part of the intended behavior of BorowerOperations and CdpManager |
||
| 119 |
/// @dev Liquidator reward shares are added when a cdp is opened, and removed when it is closed |
||
| 120 |
/// @dev closeCdp() or liqudations result in the actor (borrower or liquidator respectively) receiving the liquidator reward shares |
||
| 121 |
/// @dev Redemptions result in the shares being sent to the coll surplus pool for claiming by the CDP owner |
||
| 122 |
/// @dev Note that funds in the coll surplus pool, just like liquidator reward shares, are not tracked as part of the system CR or coll of a CDP. |
||
| 123 |
/// @dev Requires that the caller is either BorrowerOperations or CdpManager |
||
| 124 |
/// @param _account The address of the account to send systemCollShares and the liquidator reward to |
||
| 125 |
/// @param _shares The amount of systemCollShares to send |
||
| 126 |
/// @param _liquidatorRewardShares The amount of the liquidator reward shares to send |
||
| 127 | |||
| 128 |
function transferSystemCollSharesAndLiquidatorReward( |
||
| 129 |
address _account, |
||
| 130 |
uint256 _shares, |
||
| 131 |
uint256 _liquidatorRewardShares |
||
| 132 |
) external override {
|
||
| 133 |
√
|
⟳
|
_requireCallerIsBOorCdpM(); |
| 134 | |||
| 135 |
√
|
⟳
|
uint256 cachedSystemCollShares = systemCollShares; |
| 136 |
√
|
⟳
|
require(cachedSystemCollShares >= _shares, "ActivePool: Insufficient collateral shares"); |
| 137 |
√
|
⟳
|
uint256 totalShares = _shares + _liquidatorRewardShares; // TODO: Is this safe? |
| 138 |
unchecked {
|
||
| 139 |
// Safe per the check above |
||
| 140 |
√
|
⟳
|
cachedSystemCollShares -= _shares; |
| 141 |
} |
||
| 142 |
√
|
⟳
|
systemCollShares = cachedSystemCollShares; |
| 143 | |||
| 144 |
√
|
⟳
|
emit SystemCollSharesUpdated(cachedSystemCollShares); |
| 145 |
√
|
⟳
|
emit CollSharesTransferred(_account, totalShares); |
| 146 | |||
| 147 |
√
|
⟳
|
_transferCollSharesWithContractHooks(_account, totalShares); |
| 148 |
} |
||
| 149 | |||
| 150 |
/// @notice Allocate stETH shares from the system to the fee recipient to claim at-will (pull model) |
||
| 151 |
/// @dev Requires that the caller is CdpManager |
||
| 152 |
/// @dev Only the current fee recipient address is able to claim the shares |
||
| 153 |
/// @dev If the fee recipient address is changed while outstanding claimable coll is available, only the new fee recipient will be able to claim the outstanding coll |
||
| 154 |
/// @param _shares The amount of systemCollShares to allocate to the fee recipient |
||
| 155 | |||
| 156 |
function allocateSystemCollSharesToFeeRecipient(uint256 _shares) external override {
|
||
| 157 |
√
|
⟳
|
_requireCallerIsCdpManager(); |
| 158 | |||
| 159 |
√
|
⟳
|
uint256 cachedSystemCollShares = systemCollShares; |
| 160 | |||
| 161 |
√
|
⟳
|
require(cachedSystemCollShares >= _shares, "ActivePool: Insufficient collateral shares"); |
| 162 |
unchecked {
|
||
| 163 |
// Safe per the check above |
||
| 164 |
√
|
⟳
|
cachedSystemCollShares -= _shares; |
| 165 |
} |
||
| 166 | |||
| 167 |
√
|
⟳
|
systemCollShares = cachedSystemCollShares; |
| 168 | |||
| 169 |
√
|
⟳
|
uint256 cachedFeeRecipientCollShares = feeRecipientCollShares + _shares; |
| 170 |
√
|
⟳
|
feeRecipientCollShares = cachedFeeRecipientCollShares; |
| 171 | |||
| 172 |
√
|
⟳
|
emit SystemCollSharesUpdated(cachedSystemCollShares); |
| 173 |
√
|
⟳
|
emit FeeRecipientClaimableCollSharesIncreased(cachedFeeRecipientCollShares, _shares); |
| 174 |
} |
||
| 175 | |||
| 176 |
/// @notice Helper function to transfer stETH shares to another address, ensuring to call hooks into other system pools if they are the recipient |
||
| 177 |
/// @param _account The address to transfer shares to |
||
| 178 |
/// @param _shares The amount of shares to transfer |
||
| 179 | |||
| 180 |
function _transferCollSharesWithContractHooks(address _account, uint256 _shares) internal {
|
||
| 181 |
// NOTE: No need for safe transfer if the collateral asset is standard. Make sure this is the case! |
||
| 182 |
√
|
⟳
|
collateral.transferShares(_account, _shares); |
| 183 | |||
| 184 |
√
|
⟳
|
if (_account == collSurplusPoolAddress) {
|
| 185 |
⟳
|
ICollSurplusPool(_account).increaseTotalSurplusCollShares(_shares); |
|
| 186 |
} |
||
| 187 |
} |
||
| 188 | |||
| 189 |
/// @notice Increases the tracked EBTC debt of the system by a specified amount |
||
| 190 |
/// @dev Managed by system contracts - requires that the caller is either BorrowerOperations or CdpManager |
||
| 191 |
/// @param _amount: The amount to increase the system EBTC debt by |
||
| 192 | |||
| 193 |
function increaseSystemDebt(uint256 _amount) external override {
|
||
| 194 |
√
|
_requireCallerIsBOorCdpM(); |
|
| 195 | |||
| 196 |
√
|
uint256 cachedSystemDebt = systemDebt + _amount; |
|
| 197 | |||
| 198 |
√
|
systemDebt = cachedSystemDebt; |
|
| 199 |
√
|
emit ActivePoolEBTCDebtUpdated(cachedSystemDebt); |
|
| 200 |
} |
||
| 201 | |||
| 202 |
/// @notice Decreases the tracked EBTC debt of the system by a specified amount |
||
| 203 |
/// @dev Managed by system contracts - requires that the caller is either BorrowerOperations or CdpManager |
||
| 204 |
/// @param _amount: The amount to decrease the system EBTC debt by |
||
| 205 | |||
| 206 |
function decreaseSystemDebt(uint256 _amount) external override {
|
||
| 207 |
√
|
⟳
|
_requireCallerIsBOorCdpM(); |
| 208 | |||
| 209 |
√
|
⟳
|
uint256 cachedSystemDebt = systemDebt - _amount; |
| 210 | |||
| 211 |
√
|
⟳
|
systemDebt = cachedSystemDebt; |
| 212 |
√
|
⟳
|
emit ActivePoolEBTCDebtUpdated(cachedSystemDebt); |
| 213 |
} |
||
| 214 | |||
| 215 |
// --- 'require' functions --- |
||
| 216 | |||
| 217 |
/// @notice Checks if the caller is BorrowerOperations |
||
| 218 |
function _requireCallerIsBorrowerOperations() internal view {
|
||
| 219 |
require( |
||
| 220 |
√
|
msg.sender == borrowerOperationsAddress, |
|
| 221 |
"ActivePool: Caller is not BorrowerOperations" |
||
| 222 |
); |
||
| 223 |
} |
||
| 224 | |||
| 225 |
/// @notice Checks if the caller is either BorrowerOperations or CdpManager |
||
| 226 |
function _requireCallerIsBOorCdpM() internal view {
|
||
| 227 |
require( |
||
| 228 |
√
|
⟳
|
msg.sender == borrowerOperationsAddress || msg.sender == cdpManagerAddress, |
| 229 |
"ActivePool: Caller is neither BorrowerOperations nor CdpManager" |
||
| 230 |
); |
||
| 231 |
} |
||
| 232 | |||
| 233 |
/// @notice Checks if the caller is CdpManager |
||
| 234 |
function _requireCallerIsCdpManager() internal view {
|
||
| 235 |
√
|
⟳
|
require(msg.sender == cdpManagerAddress, "ActivePool: Caller is not CdpManager"); |
| 236 |
} |
||
| 237 | |||
| 238 |
/// @notice Notify that stETH collateral shares have been recieved, updating internal accounting accordingly |
||
| 239 |
/// @param _value The amount of collateral to receive |
||
| 240 | |||
| 241 |
function increaseSystemCollShares(uint256 _value) external override {
|
||
| 242 |
√
|
_requireCallerIsBorrowerOperations(); |
|
| 243 | |||
| 244 |
√
|
uint256 cachedSystemCollShares = systemCollShares + _value; |
|
| 245 |
√
|
systemCollShares = cachedSystemCollShares; |
|
| 246 |
√
|
emit SystemCollSharesUpdated(cachedSystemCollShares); |
|
| 247 |
} |
||
| 248 | |||
| 249 |
// === Flashloans === // |
||
| 250 | |||
| 251 |
/// @notice Borrow assets with a flash loan |
||
| 252 |
/// @dev The Collateral checks may cause reverts if you trigger a fee change big enough |
||
| 253 |
/// consider calling `cdpManagerAddress.syncGlobalAccountingAndGracePeriod()` |
||
| 254 |
/// @param receiver The address to receive the flash loan |
||
| 255 |
/// @param token The address of the token to loan |
||
| 256 |
/// @param amount The amount of tokens to loan |
||
| 257 |
/// @param data Additional data |
||
| 258 |
/// @return A boolean value indicating whether the operation was successful |
||
| 259 | |||
| 260 |
function flashLoan( |
||
| 261 |
IERC3156FlashBorrower receiver, |
||
| 262 |
address token, |
||
| 263 |
uint256 amount, |
||
| 264 |
bytes calldata data |
||
| 265 |
) external override returns (bool) {
|
||
| 266 |
require(amount > 0, "ActivePool: 0 Amount"); |
||
| 267 |
uint256 fee = flashFee(token, amount); // NOTE: Check for `token` is implicit in the requires above // also checks for paused |
||
| 268 |
require(amount <= maxFlashLoan(token), "ActivePool: Too much"); |
||
| 269 | |||
| 270 |
uint256 amountWithFee = amount + fee; |
||
| 271 |
uint256 oldRate = collateral.getPooledEthByShares(DECIMAL_PRECISION); |
||
| 272 | |||
| 273 |
collateral.transfer(address(receiver), amount); |
||
| 274 | |||
| 275 |
// Callback |
||
| 276 |
require( |
||
| 277 |
receiver.onFlashLoan(msg.sender, token, amount, fee, data) == FLASH_SUCCESS_VALUE, |
||
| 278 |
"ActivePool: IERC3156: Callback failed" |
||
| 279 |
); |
||
| 280 | |||
| 281 |
// Transfer of (principal + Fee) from flashloan receiver |
||
| 282 |
collateral.transferFrom(address(receiver), address(this), amountWithFee); |
||
| 283 | |||
| 284 |
// Send earned fee to designated recipient |
||
| 285 |
collateral.transfer(feeRecipientAddress, fee); |
||
| 286 | |||
| 287 |
// Check new balance |
||
| 288 |
// NOTE: Invariant Check, technically breaks CEI but I think we must use it |
||
| 289 |
// NOTE: This means any balance > systemCollShares is stuck, this is also present in LUSD as is |
||
| 290 | |||
| 291 |
// NOTE: This check effectively prevents running 2 FL at the same time |
||
| 292 |
// You technically could, but you'd be having to repay any amount below systemCollShares to get Fl2 to not revert |
||
| 293 |
require( |
||
| 294 |
collateral.balanceOf(address(this)) >= collateral.getPooledEthByShares(systemCollShares), |
||
| 295 |
"ActivePool: Must repay Balance" |
||
| 296 |
); |
||
| 297 |
require( |
||
| 298 |
collateral.sharesOf(address(this)) >= systemCollShares, |
||
| 299 |
"ActivePool: Must repay Share" |
||
| 300 |
); |
||
| 301 |
require( |
||
| 302 |
collateral.getPooledEthByShares(DECIMAL_PRECISION) == oldRate, |
||
| 303 |
"ActivePool: Should keep same collateral share rate" |
||
| 304 |
); |
||
| 305 | |||
| 306 |
emit FlashLoanSuccess(address(receiver), token, amount, fee); |
||
| 307 | |||
| 308 |
return true; |
||
| 309 |
} |
||
| 310 | |||
| 311 |
/// @notice Calculate the flash loan fee for a given token and amount loaned |
||
| 312 |
/// @param token The address of the token to calculate the fee for |
||
| 313 |
/// @param amount The amount of tokens to calculate the fee for |
||
| 314 |
/// @return The amount of the flash loan fee |
||
| 315 | |||
| 316 |
function flashFee(address token, uint256 amount) public view override returns (uint256) {
|
||
| 317 |
require(token == address(collateral), "ActivePool: collateral Only"); |
||
| 318 |
require(!flashLoansPaused, "ActivePool: Flash Loans Paused"); |
||
| 319 | |||
| 320 |
return (amount * feeBps) / MAX_BPS; |
||
| 321 |
} |
||
| 322 | |||
| 323 |
/// @notice Get the maximum flash loan amount for a specific token |
||
| 324 |
/// @dev Exclusively used here for stETH collateral, equal to the current balance of the pool |
||
| 325 |
/// @param token The address of the token to get the maximum flash loan amount for |
||
| 326 |
/// @return The maximum flash loan amount for the token |
||
| 327 |
function maxFlashLoan(address token) public view override returns (uint256) {
|
||
| 328 |
if (token != address(collateral)) {
|
||
| 329 |
return 0; |
||
| 330 |
} |
||
| 331 | |||
| 332 |
if (flashLoansPaused) {
|
||
| 333 |
return 0; |
||
| 334 |
} |
||
| 335 | |||
| 336 |
return collateral.balanceOf(address(this)); |
||
| 337 |
} |
||
| 338 | |||
| 339 |
// === Governed Functions === // |
||
| 340 | |||
| 341 |
/// @notice Claim outstanding shares for fee recipient, updating internal accounting and transferring the shares. |
||
| 342 |
/// @dev Call permissinos are managed via authority for flexibility, rather than gating call to just feeRecipient. |
||
| 343 |
/// @dev Is likely safe as an open permission though caution should be taken. |
||
| 344 |
/// @param _shares The amount of shares to claim to feeRecipient |
||
| 345 |
function claimFeeRecipientCollShares(uint256 _shares) external override requiresAuth {
|
||
| 346 |
√
|
⟳
|
ICdpManagerData(cdpManagerAddress).syncGlobalAccountingAndGracePeriod(); // Calling this increases shares so do it first |
| 347 | |||
| 348 |
√
|
⟳
|
uint256 cachedFeeRecipientCollShares = feeRecipientCollShares; |
| 349 |
require( |
||
| 350 |
√
|
⟳
|
cachedFeeRecipientCollShares >= _shares, |
| 351 |
"ActivePool: Insufficient fee recipient coll" |
||
| 352 |
); |
||
| 353 | |||
| 354 |
unchecked {
|
||
| 355 |
√
|
⟳
|
cachedFeeRecipientCollShares -= _shares; |
| 356 |
} |
||
| 357 | |||
| 358 |
√
|
⟳
|
feeRecipientCollShares = cachedFeeRecipientCollShares; |
| 359 |
√
|
⟳
|
emit FeeRecipientClaimableCollSharesDecreased(cachedFeeRecipientCollShares, _shares); |
| 360 | |||
| 361 |
√
|
⟳
|
collateral.transferShares(feeRecipientAddress, _shares); |
| 362 |
} |
||
| 363 | |||
| 364 |
/// @dev Function to move unintended dust that are not protected |
||
| 365 |
/// @notice moves given amount of given token (collateral is NOT allowed) |
||
| 366 |
/// @notice because recipient are fixed, this function is safe to be called by anyone |
||
| 367 | |||
| 368 |
function sweepToken(address token, uint256 amount) public nonReentrant requiresAuth {
|
||
| 369 |
ICdpManagerData(cdpManagerAddress).syncGlobalAccountingAndGracePeriod(); // Accrue State First |
||
| 370 | |||
| 371 |
require(token != address(collateral), "ActivePool: Cannot Sweep Collateral"); |
||
| 372 | |||
| 373 |
uint256 balance = IERC20(token).balanceOf(address(this)); |
||
| 374 |
require(amount <= balance, "ActivePool: Attempt to sweep more than balance"); |
||
| 375 | |||
| 376 |
address cachedFeeRecipientAddress = feeRecipientAddress; // Saves an SLOAD |
||
| 377 | |||
| 378 |
IERC20(token).safeTransfer(cachedFeeRecipientAddress, amount); |
||
| 379 | |||
| 380 |
emit SweepTokenSuccess(token, amount, cachedFeeRecipientAddress); |
||
| 381 |
} |
||
| 382 | |||
| 383 |
/// @notice Set new FeeRecipient |
||
| 384 |
/// @dev Previous fees are forfeited, if you wish to claim to previous address |
||
| 385 |
/// call `claimFeeRecipientCollShares` first |
||
| 386 |
function setFeeRecipientAddress(address _feeRecipientAddress) external requiresAuth {
|
||
| 387 |
ICdpManagerData(cdpManagerAddress).syncGlobalAccountingAndGracePeriod(); // Accrue State First |
||
| 388 | |||
| 389 |
require( |
||
| 390 |
_feeRecipientAddress != address(0), |
||
| 391 |
"ActivePool: Cannot set fee recipient to zero address" |
||
| 392 |
); |
||
| 393 | |||
| 394 |
feeRecipientAddress = _feeRecipientAddress; |
||
| 395 |
emit FeeRecipientAddressChanged(_feeRecipientAddress); |
||
| 396 |
} |
||
| 397 | |||
| 398 |
/// @notice Sets new Fee for FlashLoans |
||
| 399 |
function setFeeBps(uint256 _newFee) external requiresAuth {
|
||
| 400 |
ICdpManagerData(cdpManagerAddress).syncGlobalAccountingAndGracePeriod(); // Accrue State First |
||
| 401 | |||
| 402 |
require(_newFee <= MAX_FEE_BPS, "ERC3156FlashLender: _newFee should <= MAX_FEE_BPS"); |
||
| 403 | |||
| 404 |
// set new flash fee |
||
| 405 |
uint256 _oldFee = feeBps; |
||
| 406 |
feeBps = uint16(_newFee); |
||
| 407 |
emit FlashFeeSet(msg.sender, _oldFee, _newFee); |
||
| 408 |
} |
||
| 409 | |||
| 410 |
/// @notice Should Flashloans be paused? |
||
| 411 |
function setFlashLoansPaused(bool _paused) external requiresAuth {
|
||
| 412 |
ICdpManagerData(cdpManagerAddress).syncGlobalAccountingAndGracePeriod(); // Accrue State First |
||
| 413 | |||
| 414 |
flashLoansPaused = _paused; |
||
| 415 |
emit FlashLoansPaused(msg.sender, _paused); |
||
| 416 |
} |
||
| 417 |
} |
||
| 418 |
| Lines covered: | 232 / 331 (70.1%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Interfaces/IBorrowerOperations.sol"; |
||
| 6 |
import "./Interfaces/ICdpManager.sol"; |
||
| 7 |
import "./Interfaces/ICdpManagerData.sol"; |
||
| 8 |
import "./Interfaces/IEBTCToken.sol"; |
||
| 9 |
import "./Interfaces/ICollSurplusPool.sol"; |
||
| 10 |
import "./Interfaces/ISortedCdps.sol"; |
||
| 11 |
import "./Dependencies/EbtcBase.sol"; |
||
| 12 |
import "./Dependencies/ReentrancyGuard.sol"; |
||
| 13 |
import "./Dependencies/Ownable.sol"; |
||
| 14 |
import "./Dependencies/AuthNoOwner.sol"; |
||
| 15 |
import "./Dependencies/ERC3156FlashLender.sol"; |
||
| 16 |
import "./Dependencies/PermitNonce.sol"; |
||
| 17 | |||
| 18 |
contract BorrowerOperations is |
||
| 19 |
EbtcBase, |
||
| 20 |
ReentrancyGuard, |
||
| 21 |
IBorrowerOperations, |
||
| 22 |
ERC3156FlashLender, |
||
| 23 |
AuthNoOwner, |
||
| 24 |
PermitNonce |
||
| 25 |
{
|
||
| 26 |
string public constant NAME = "BorrowerOperations"; |
||
| 27 | |||
| 28 |
// keccak256("permitPositionManagerApproval(address borrower,address positionManager,uint8 status,uint256 nonce,uint256 deadline)");
|
||
| 29 |
bytes32 private constant _PERMIT_POSITION_MANAGER_TYPEHASH = |
||
| 30 |
keccak256( |
||
| 31 |
"PermitPositionManagerApproval(address borrower,address positionManager,uint8 status,uint256 nonce,uint256 deadline)" |
||
| 32 |
); |
||
| 33 | |||
| 34 |
// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
|
||
| 35 |
bytes32 private constant _TYPE_HASH = |
||
| 36 |
√
|
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; |
|
| 37 | |||
| 38 |
string internal constant _VERSION = "1"; |
||
| 39 | |||
| 40 |
// Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to |
||
| 41 |
// invalidate the cached domain separator if the chain id changes. |
||
| 42 |
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; |
||
| 43 |
uint256 private immutable _CACHED_CHAIN_ID; |
||
| 44 | |||
| 45 |
bytes32 private immutable _HASHED_NAME; |
||
| 46 |
bytes32 private immutable _HASHED_VERSION; |
||
| 47 | |||
| 48 |
// --- Connected contract declarations --- |
||
| 49 | |||
| 50 |
ICdpManager public immutable cdpManager; |
||
| 51 | |||
| 52 |
ICollSurplusPool public immutable collSurplusPool; |
||
| 53 | |||
| 54 |
address public feeRecipientAddress; |
||
| 55 | |||
| 56 |
IEBTCToken public immutable ebtcToken; |
||
| 57 | |||
| 58 |
// A doubly linked list of Cdps, sorted by their collateral ratios |
||
| 59 |
ISortedCdps public immutable sortedCdps; |
||
| 60 | |||
| 61 |
// Mapping of borrowers to approved position managers, by approval status: cdpOwner(borrower) -> positionManager -> PositionManagerApproval (None, OneTime, Persistent) |
||
| 62 |
mapping(address => mapping(address => PositionManagerApproval)) public positionManagers; |
||
| 63 | |||
| 64 |
/* --- Variable container structs --- |
||
| 65 | |||
| 66 |
Used to hold, return and assign variables inside a function, in order to avoid the error: |
||
| 67 |
"CompilerError: Stack too deep". */ |
||
| 68 | |||
| 69 |
struct LocalVariables_adjustCdp {
|
||
| 70 |
uint256 price; |
||
| 71 |
uint256 collChange; |
||
| 72 |
uint256 netDebtChange; |
||
| 73 |
bool isCollIncrease; |
||
| 74 |
uint256 debt; |
||
| 75 |
uint256 coll; |
||
| 76 |
uint256 oldICR; |
||
| 77 |
uint256 newICR; |
||
| 78 |
uint256 newTCR; |
||
| 79 |
uint256 newDebt; |
||
| 80 |
uint256 newColl; |
||
| 81 |
uint256 stake; |
||
| 82 |
} |
||
| 83 | |||
| 84 |
struct LocalVariables_openCdp {
|
||
| 85 |
uint256 price; |
||
| 86 |
uint256 debt; |
||
| 87 |
uint256 totalColl; |
||
| 88 |
uint256 netColl; |
||
| 89 |
uint256 ICR; |
||
| 90 |
uint256 NICR; |
||
| 91 |
uint256 stake; |
||
| 92 |
uint256 arrayIndex; |
||
| 93 |
} |
||
| 94 | |||
| 95 |
struct LocalVariables_moveTokens {
|
||
| 96 |
address user; |
||
| 97 |
uint256 collChange; |
||
| 98 |
uint256 collAddUnderlying; // ONLY for isCollIncrease=true |
||
| 99 |
bool isCollIncrease; |
||
| 100 |
uint256 EBTCChange; |
||
| 101 |
bool isDebtIncrease; |
||
| 102 |
uint256 netDebtChange; |
||
| 103 |
} |
||
| 104 | |||
| 105 |
// --- Dependency setters --- |
||
| 106 |
constructor( |
||
| 107 |
address _cdpManagerAddress, |
||
| 108 |
address _activePoolAddress, |
||
| 109 |
address _collSurplusPoolAddress, |
||
| 110 |
address _priceFeedAddress, |
||
| 111 |
address _sortedCdpsAddress, |
||
| 112 |
address _ebtcTokenAddress, |
||
| 113 |
address _feeRecipientAddress, |
||
| 114 |
address _collTokenAddress |
||
| 115 |
√
|
) EbtcBase(_activePoolAddress, _priceFeedAddress, _collTokenAddress) {
|
|
| 116 |
√
|
cdpManager = ICdpManager(_cdpManagerAddress); |
|
| 117 |
√
|
collSurplusPool = ICollSurplusPool(_collSurplusPoolAddress); |
|
| 118 |
√
|
sortedCdps = ISortedCdps(_sortedCdpsAddress); |
|
| 119 |
√
|
ebtcToken = IEBTCToken(_ebtcTokenAddress); |
|
| 120 |
√
|
feeRecipientAddress = _feeRecipientAddress; |
|
| 121 | |||
| 122 |
√
|
address _authorityAddress = address(AuthNoOwner(_cdpManagerAddress).authority()); |
|
| 123 |
√
|
if (_authorityAddress != address(0)) {
|
|
| 124 |
√
|
_initializeAuthority(_authorityAddress); |
|
| 125 |
} |
||
| 126 | |||
| 127 |
√
|
bytes32 hashedName = keccak256(bytes(NAME)); |
|
| 128 |
√
|
bytes32 hashedVersion = keccak256(bytes(_VERSION)); |
|
| 129 | |||
| 130 |
√
|
_HASHED_NAME = hashedName; |
|
| 131 |
√
|
_HASHED_VERSION = hashedVersion; |
|
| 132 |
√
|
_CACHED_CHAIN_ID = _chainID(); |
|
| 133 |
√
|
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(_TYPE_HASH, hashedName, hashedVersion); |
|
| 134 | |||
| 135 |
√
|
emit FeeRecipientAddressChanged(_feeRecipientAddress); |
|
| 136 |
} |
||
| 137 | |||
| 138 |
/** |
||
| 139 |
@notice BorrowerOperations and CdpManager share reentrancy status by confirming the other's locked flag before beginning operation |
||
| 140 |
@dev This is an alternative to the more heavyweight solution of both being able to set the reentrancy flag on a 3rd contract. |
||
| 141 |
@dev Prevents multi-contract reentrancy between these two contracts |
||
| 142 |
*/ |
||
| 143 |
modifier nonReentrantSelfAndCdpM() {
|
||
| 144 |
√
|
⟳
|
require(locked == OPEN, "BorrowerOperations: Reentrancy in nonReentrant call"); |
| 145 |
require( |
||
| 146 |
√
|
⟳
|
ReentrancyGuard(address(cdpManager)).locked() == OPEN, |
| 147 |
"CdpManager: Reentrancy in nonReentrant call" |
||
| 148 |
); |
||
| 149 | |||
| 150 |
√
|
⟳
|
locked = LOCKED; |
| 151 | |||
| 152 |
_; |
||
| 153 | |||
| 154 |
√
|
locked = OPEN; |
|
| 155 |
} |
||
| 156 | |||
| 157 |
// --- Borrower Cdp Operations --- |
||
| 158 | |||
| 159 |
/** |
||
| 160 |
@notice Function that creates a Cdp for the caller with the requested debt, and the stETH received as collateral. |
||
| 161 |
@notice Successful execution is conditional mainly on the resulting collateralization ratio which must exceed the minimum (110% in Normal Mode, 150% in Recovery Mode). |
||
| 162 |
@notice In addition to the requested debt, extra debt is issued to cover the gas compensation. |
||
| 163 |
*/ |
||
| 164 |
function openCdp( |
||
| 165 |
uint256 _debt, |
||
| 166 |
bytes32 _upperHint, |
||
| 167 |
bytes32 _lowerHint, |
||
| 168 |
uint256 _stEthBalance |
||
| 169 |
√
|
⟳
|
) external override nonReentrantSelfAndCdpM returns (bytes32) {
|
| 170 |
√
|
⟳
|
return _openCdp(_debt, _upperHint, _lowerHint, _stEthBalance, msg.sender); |
| 171 |
} |
||
| 172 | |||
| 173 |
function openCdpFor( |
||
| 174 |
uint256 _debt, |
||
| 175 |
bytes32 _upperHint, |
||
| 176 |
bytes32 _lowerHint, |
||
| 177 |
uint256 _collAmount, |
||
| 178 |
address _borrower |
||
| 179 |
) external override nonReentrantSelfAndCdpM returns (bytes32) {
|
||
| 180 |
return _openCdp(_debt, _upperHint, _lowerHint, _collAmount, _borrower); |
||
| 181 |
} |
||
| 182 | |||
| 183 |
// Function that adds the received stETH to the caller's specified Cdp. |
||
| 184 |
function addColl( |
||
| 185 |
bytes32 _cdpId, |
||
| 186 |
bytes32 _upperHint, |
||
| 187 |
bytes32 _lowerHint, |
||
| 188 |
uint256 _stEthBalanceIncrease |
||
| 189 |
) external override nonReentrantSelfAndCdpM {
|
||
| 190 |
√
|
_adjustCdpInternal(_cdpId, 0, 0, false, _upperHint, _lowerHint, _stEthBalanceIncrease); |
|
| 191 |
} |
||
| 192 | |||
| 193 |
/** |
||
| 194 |
Withdraws `_stEthBalanceDecrease` amount of collateral from the caller’s Cdp. Executes only if the user has an active Cdp, the withdrawal would not pull the user’s Cdp below the minimum collateralization ratio, and the resulting total collateralization ratio of the system is above 150%. |
||
| 195 |
*/ |
||
| 196 |
function withdrawColl( |
||
| 197 |
bytes32 _cdpId, |
||
| 198 |
uint256 _stEthBalanceDecrease, |
||
| 199 |
bytes32 _upperHint, |
||
| 200 |
bytes32 _lowerHint |
||
| 201 |
) external override nonReentrantSelfAndCdpM {
|
||
| 202 |
√
|
⟳
|
_adjustCdpInternal(_cdpId, _stEthBalanceDecrease, 0, false, _upperHint, _lowerHint, 0); |
| 203 |
} |
||
| 204 | |||
| 205 |
// Withdraw EBTC tokens from a cdp: mint new EBTC tokens to the owner, and increase the cdp's debt accordingly |
||
| 206 |
/** |
||
| 207 |
Issues `_amount` of eBTC from the caller’s Cdp to the caller. Executes only if the Cdp's collateralization ratio would remain above the minimum, and the resulting total collateralization ratio is above 150%. |
||
| 208 |
*/ |
||
| 209 |
function withdrawDebt( |
||
| 210 |
bytes32 _cdpId, |
||
| 211 |
uint256 _debt, |
||
| 212 |
bytes32 _upperHint, |
||
| 213 |
bytes32 _lowerHint |
||
| 214 |
) external override nonReentrantSelfAndCdpM {
|
||
| 215 |
√
|
⟳
|
_adjustCdpInternal(_cdpId, 0, _debt, true, _upperHint, _lowerHint, 0); |
| 216 |
} |
||
| 217 | |||
| 218 |
// Repay EBTC tokens to a Cdp: Burn the repaid EBTC tokens, and reduce the cdp's debt accordingly |
||
| 219 |
/** |
||
| 220 |
repay `_amount` of eBTC to the caller’s Cdp, subject to leaving 50 debt in the Cdp (which corresponds to the 50 eBTC gas compensation). |
||
| 221 |
*/ |
||
| 222 |
function repayDebt( |
||
| 223 |
bytes32 _cdpId, |
||
| 224 |
uint256 _debt, |
||
| 225 |
bytes32 _upperHint, |
||
| 226 |
bytes32 _lowerHint |
||
| 227 |
) external override nonReentrantSelfAndCdpM {
|
||
| 228 |
√
|
⟳
|
_adjustCdpInternal(_cdpId, 0, _debt, false, _upperHint, _lowerHint, 0); |
| 229 |
} |
||
| 230 | |||
| 231 |
function adjustCdp( |
||
| 232 |
bytes32 _cdpId, |
||
| 233 |
uint256 _stEthBalanceDecrease, |
||
| 234 |
uint256 _debtChange, |
||
| 235 |
bool _isDebtIncrease, |
||
| 236 |
bytes32 _upperHint, |
||
| 237 |
bytes32 _lowerHint |
||
| 238 |
) external override nonReentrantSelfAndCdpM {
|
||
| 239 |
√
|
⟳
|
_adjustCdpInternal( |
| 240 |
√
|
⟳
|
_cdpId, |
| 241 |
√
|
⟳
|
_stEthBalanceDecrease, |
| 242 |
√
|
⟳
|
_debtChange, |
| 243 |
√
|
⟳
|
_isDebtIncrease, |
| 244 |
√
|
⟳
|
_upperHint, |
| 245 |
√
|
⟳
|
_lowerHint, |
| 246 |
√
|
⟳
|
0 |
| 247 |
); |
||
| 248 |
} |
||
| 249 | |||
| 250 |
/** |
||
| 251 |
enables a borrower to simultaneously change both their collateral and debt, subject to all the restrictions that apply to individual increases/decreases of each quantity with the following particularity: if the adjustment reduces the collateralization ratio of the Cdp, the function only executes if the resulting total collateralization ratio is above 150%. The borrower has to provide a `_maxFeePercentage` that he/she is willing to accept in case of a fee slippage, i.e. when a redemption transaction is processed first, driving up the issuance fee. The parameter is ignored if the debt is not increased with the transaction. |
||
| 252 |
*/ |
||
| 253 |
// TODO optimization candidate |
||
| 254 |
function adjustCdpWithColl( |
||
| 255 |
bytes32 _cdpId, |
||
| 256 |
uint256 _stEthBalanceDecrease, |
||
| 257 |
uint256 _debtChange, |
||
| 258 |
bool _isDebtIncrease, |
||
| 259 |
bytes32 _upperHint, |
||
| 260 |
bytes32 _lowerHint, |
||
| 261 |
uint256 _stEthBalanceIncrease |
||
| 262 |
) external override nonReentrantSelfAndCdpM {
|
||
| 263 |
_adjustCdpInternal( |
||
| 264 |
_cdpId, |
||
| 265 |
_stEthBalanceDecrease, |
||
| 266 |
_debtChange, |
||
| 267 |
_isDebtIncrease, |
||
| 268 |
_upperHint, |
||
| 269 |
_lowerHint, |
||
| 270 |
_stEthBalanceIncrease |
||
| 271 |
); |
||
| 272 |
} |
||
| 273 | |||
| 274 |
/* |
||
| 275 |
* _adjustCdpInternal(): Alongside a debt change, this function can perform either |
||
| 276 |
* a collateral top-up or a collateral withdrawal. |
||
| 277 |
* |
||
| 278 |
* It therefore expects either a positive _stEthBalanceIncrease, or a positive _stEthBalanceDecrease argument. |
||
| 279 |
* |
||
| 280 |
* If both are positive, it will revert. |
||
| 281 |
*/ |
||
| 282 |
function _adjustCdpInternal( |
||
| 283 |
bytes32 _cdpId, |
||
| 284 |
uint256 _stEthBalanceDecrease, |
||
| 285 |
uint256 _debtChange, |
||
| 286 |
bool _isDebtIncrease, |
||
| 287 |
bytes32 _upperHint, |
||
| 288 |
bytes32 _lowerHint, |
||
| 289 |
uint256 _stEthBalanceIncrease |
||
| 290 |
) internal {
|
||
| 291 |
// Confirm the operation is the borrower or approved position manager adjusting its own cdp |
||
| 292 |
√
|
⟳
|
address _borrower = sortedCdps.getOwnerAddress(_cdpId); |
| 293 |
√
|
⟳
|
_requireBorrowerOrPositionManagerAndUpdate(_borrower); |
| 294 | |||
| 295 |
√
|
⟳
|
_requireCdpisActive(cdpManager, _cdpId); |
| 296 | |||
| 297 |
√
|
⟳
|
cdpManager.syncAccounting(_cdpId); |
| 298 | |||
| 299 |
√
|
⟳
|
LocalVariables_adjustCdp memory vars; |
| 300 | |||
| 301 |
√
|
⟳
|
vars.price = priceFeed.fetchPrice(); |
| 302 |
√
|
⟳
|
bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(vars.price)); |
| 303 | |||
| 304 |
√
|
⟳
|
if (_isDebtIncrease) {
|
| 305 |
√
|
⟳
|
_requireNonZeroDebtChange(_debtChange); |
| 306 |
} |
||
| 307 |
√
|
⟳
|
_requireSingularCollChange(_stEthBalanceIncrease, _stEthBalanceDecrease); |
| 308 |
√
|
⟳
|
_requireNonZeroAdjustment(_stEthBalanceIncrease, _stEthBalanceDecrease, _debtChange); |
| 309 | |||
| 310 |
// Get the collChange based on the collateral value transferred in the transaction |
||
| 311 |
√
|
⟳
|
(vars.collChange, vars.isCollIncrease) = _getCollSharesChangeFromStEthChange( |
| 312 |
√
|
⟳
|
_stEthBalanceIncrease, |
| 313 |
√
|
⟳
|
_stEthBalanceDecrease |
| 314 |
); |
||
| 315 | |||
| 316 |
√
|
⟳
|
vars.netDebtChange = _debtChange; |
| 317 | |||
| 318 |
√
|
⟳
|
vars.debt = cdpManager.getCdpDebt(_cdpId); |
| 319 |
√
|
⟳
|
vars.coll = cdpManager.getCdpCollShares(_cdpId); |
| 320 | |||
| 321 |
// Get the cdp's old ICR before the adjustment, and what its new ICR will be after the adjustment |
||
| 322 |
√
|
⟳
|
uint256 _cdpStEthBalance = collateral.getPooledEthByShares(vars.coll); |
| 323 |
require( |
||
| 324 |
√
|
⟳
|
_stEthBalanceDecrease <= _cdpStEthBalance, |
| 325 |
"BorrowerOperations: withdraw more collateral than CDP has!" |
||
| 326 |
); |
||
| 327 |
√
|
⟳
|
vars.oldICR = EbtcMath._computeCR(_cdpStEthBalance, vars.debt, vars.price); |
| 328 |
√
|
⟳
|
vars.newICR = _getNewICRFromCdpChange( |
| 329 |
√
|
⟳
|
vars.coll, |
| 330 |
√
|
⟳
|
vars.debt, |
| 331 |
√
|
⟳
|
vars.collChange, |
| 332 |
√
|
⟳
|
vars.isCollIncrease, |
| 333 |
√
|
⟳
|
vars.netDebtChange, |
| 334 |
√
|
⟳
|
_isDebtIncrease, |
| 335 |
√
|
⟳
|
vars.price |
| 336 |
); |
||
| 337 | |||
| 338 |
// Check the adjustment satisfies all conditions for the current system mode |
||
| 339 |
√
|
⟳
|
_requireValidAdjustmentInCurrentMode( |
| 340 |
√
|
⟳
|
isRecoveryMode, |
| 341 |
√
|
⟳
|
_stEthBalanceDecrease, |
| 342 |
√
|
⟳
|
_isDebtIncrease, |
| 343 |
√
|
⟳
|
vars |
| 344 |
); |
||
| 345 | |||
| 346 |
// When the adjustment is a debt repayment, check it's a valid amount, that the caller has enough EBTC, and that the resulting debt is >0 |
||
| 347 |
√
|
⟳
|
if (!_isDebtIncrease && _debtChange > 0) {
|
| 348 |
√
|
⟳
|
_requireValidDebtRepayment(vars.debt, vars.netDebtChange); |
| 349 |
√
|
⟳
|
_requireSufficientEbtcBalance(msg.sender, vars.netDebtChange); |
| 350 |
√
|
⟳
|
_requireNonZeroDebt(vars.debt - vars.netDebtChange); |
| 351 |
} |
||
| 352 | |||
| 353 |
√
|
⟳
|
(vars.newColl, vars.newDebt) = _getNewCdpAmounts( |
| 354 |
√
|
⟳
|
vars.coll, |
| 355 |
√
|
⟳
|
vars.debt, |
| 356 |
√
|
⟳
|
vars.collChange, |
| 357 |
√
|
⟳
|
vars.isCollIncrease, |
| 358 |
√
|
⟳
|
vars.netDebtChange, |
| 359 |
√
|
⟳
|
_isDebtIncrease |
| 360 |
); |
||
| 361 | |||
| 362 |
√
|
⟳
|
_requireAtLeastMinNetStEthBalance(collateral.getPooledEthByShares(vars.newColl)); |
| 363 | |||
| 364 |
√
|
cdpManager.updateCdp(_cdpId, _borrower, vars.coll, vars.debt, vars.newColl, vars.newDebt); |
|
| 365 | |||
| 366 |
// Re-insert cdp in to the sorted list |
||
| 367 |
{
|
||
| 368 |
√
|
uint256 newNICR = _getNewNominalICRFromCdpChange(vars, _isDebtIncrease); |
|
| 369 |
√
|
sortedCdps.reInsert(_cdpId, newNICR, _upperHint, _lowerHint); |
|
| 370 |
} |
||
| 371 | |||
| 372 |
// Use the unmodified _debtChange here, as we don't send the fee to the user |
||
| 373 |
{
|
||
| 374 |
√
|
LocalVariables_moveTokens memory _varMvTokens = LocalVariables_moveTokens( |
|
| 375 |
√
|
msg.sender, |
|
| 376 |
√
|
vars.collChange, |
|
| 377 |
√
|
(vars.isCollIncrease ? _stEthBalanceIncrease : 0), |
|
| 378 |
√
|
vars.isCollIncrease, |
|
| 379 |
√
|
_debtChange, |
|
| 380 |
√
|
_isDebtIncrease, |
|
| 381 |
√
|
vars.netDebtChange |
|
| 382 |
); |
||
| 383 |
√
|
_processTokenMovesFromAdjustment(_varMvTokens); |
|
| 384 |
} |
||
| 385 |
} |
||
| 386 | |||
| 387 |
function _openCdp( |
||
| 388 |
uint256 _debt, |
||
| 389 |
bytes32 _upperHint, |
||
| 390 |
bytes32 _lowerHint, |
||
| 391 |
uint256 _stEthBalance, |
||
| 392 |
address _borrower |
||
| 393 |
√
|
⟳
|
) internal returns (bytes32) {
|
| 394 |
√
|
⟳
|
_requireNonZeroDebt(_debt); |
| 395 |
√
|
⟳
|
_requireBorrowerOrPositionManagerAndUpdate(_borrower); |
| 396 | |||
| 397 |
√
|
⟳
|
LocalVariables_openCdp memory vars; |
| 398 | |||
| 399 |
// ICR is based on the net coll, i.e. the requested coll amount - fixed liquidator incentive gas comp. |
||
| 400 |
√
|
⟳
|
vars.netColl = _getNetColl(_stEthBalance); |
| 401 | |||
| 402 |
// will revert if _stEthBalance is less than MIN_NET_COLL + LIQUIDATOR_REWARD |
||
| 403 |
√
|
⟳
|
_requireAtLeastMinNetStEthBalance(vars.netColl); |
| 404 | |||
| 405 |
// Update global pending index before any operations |
||
| 406 |
√
|
⟳
|
cdpManager.syncGlobalAccounting(); |
| 407 | |||
| 408 |
√
|
⟳
|
vars.price = priceFeed.fetchPrice(); |
| 409 |
√
|
⟳
|
bool isRecoveryMode = _checkRecoveryModeForTCR(_getTCR(vars.price)); |
| 410 | |||
| 411 |
√
|
⟳
|
vars.debt = _debt; |
| 412 | |||
| 413 |
// Sanity check |
||
| 414 |
√
|
⟳
|
require(vars.netColl > 0, "BorrowerOperations: zero collateral for openCdp()!"); |
| 415 | |||
| 416 |
√
|
⟳
|
uint256 _netCollAsShares = collateral.getSharesByPooledEth(vars.netColl); |
| 417 |
√
|
⟳
|
uint256 _liquidatorRewardShares = collateral.getSharesByPooledEth(LIQUIDATOR_REWARD); |
| 418 | |||
| 419 |
// ICR is based on the net coll, i.e. the requested coll amount - fixed liquidator incentive gas comp. |
||
| 420 |
√
|
⟳
|
vars.ICR = EbtcMath._computeCR(vars.netColl, vars.debt, vars.price); |
| 421 | |||
| 422 |
// NICR uses shares to normalize NICR across CDPs opened at different pooled ETH / shares ratios |
||
| 423 |
√
|
⟳
|
vars.NICR = EbtcMath._computeNominalCR(_netCollAsShares, vars.debt); |
| 424 | |||
| 425 |
/** |
||
| 426 |
In recovery move, ICR must be greater than CCR |
||
| 427 |
CCR > MCR (125% vs 110%) |
||
| 428 | |||
| 429 |
In normal mode, ICR must be greater thatn MCR |
||
| 430 |
Additionally, the new system TCR after the CDPs addition must be >CCR |
||
| 431 |
*/ |
||
| 432 |
√
|
⟳
|
uint256 newTCR = _getNewTCRFromCdpChange(vars.netColl, true, vars.debt, true, vars.price); |
| 433 |
√
|
⟳
|
if (isRecoveryMode) {
|
| 434 |
_requireICRisNotBelowCCR(vars.ICR); |
||
| 435 | |||
| 436 |
// == Grace Period == // |
||
| 437 |
// We are in RM, Edge case is Depositing Coll could exit RM |
||
| 438 |
// We check with newTCR |
||
| 439 |
if (newTCR < CCR) {
|
||
| 440 |
// Notify RM |
||
| 441 |
cdpManager.notifyStartGracePeriod(newTCR); |
||
| 442 |
} else {
|
||
| 443 |
// Notify Back to Normal Mode |
||
| 444 |
cdpManager.notifyEndGracePeriod(newTCR); |
||
| 445 |
} |
||
| 446 |
} else {
|
||
| 447 |
√
|
⟳
|
_requireICRisNotBelowMCR(vars.ICR); |
| 448 |
√
|
⟳
|
_requireNewTCRisNotBelowCCR(newTCR); |
| 449 | |||
| 450 |
// == Grace Period == // |
||
| 451 |
// We are not in RM, no edge case, we always stay above RM |
||
| 452 |
// Always Notify Back to Normal Mode |
||
| 453 |
√
|
cdpManager.notifyEndGracePeriod(newTCR); |
|
| 454 |
} |
||
| 455 | |||
| 456 |
// Set the cdp struct's properties |
||
| 457 |
√
|
bytes32 _cdpId = sortedCdps.insert(_borrower, vars.NICR, _upperHint, _lowerHint); |
|
| 458 | |||
| 459 |
// Collision check: collisions should never occur |
||
| 460 |
// Explicitly prevent it by checking for `nonExistent` |
||
| 461 |
√
|
_requireCdpIsNonExistent(_cdpId); |
|
| 462 | |||
| 463 |
// Collateral is stored in shares form for normalization |
||
| 464 |
√
|
cdpManager.initializeCdp( |
|
| 465 |
√
|
_cdpId, |
|
| 466 |
√
|
vars.debt, |
|
| 467 |
√
|
_netCollAsShares, |
|
| 468 |
√
|
_liquidatorRewardShares, |
|
| 469 |
√
|
_borrower |
|
| 470 |
); |
||
| 471 | |||
| 472 |
// Mint the full debt amount, in eBTC tokens, to the caller |
||
| 473 |
√
|
_withdrawDebt(msg.sender, _debt, _debt); |
|
| 474 | |||
| 475 |
/** |
||
| 476 |
Note that only NET coll (as shares) is considered part of the CDP. |
||
| 477 |
The static liqudiation incentive is stored in the gas pool and can be considered a deposit / voucher to be returned upon CDP close, to the closer. |
||
| 478 |
The close can happen from the borrower closing their own CDP, a full liquidation, or a redemption. |
||
| 479 |
*/ |
||
| 480 | |||
| 481 |
// CEI: Move the net collateral and liquidator gas compensation to the Active Pool. Track only net collateral shares for TCR purposes. |
||
| 482 |
√
|
_activePoolAddColl(_stEthBalance, _netCollAsShares); |
|
| 483 | |||
| 484 |
// Invariant check |
||
| 485 |
require( |
||
| 486 |
√
|
vars.netColl + LIQUIDATOR_REWARD == _stEthBalance, |
|
| 487 |
"BorrowerOperations: deposited collateral mismatch!" |
||
| 488 |
); |
||
| 489 | |||
| 490 |
√
|
return _cdpId; |
|
| 491 |
} |
||
| 492 | |||
| 493 |
/** |
||
| 494 |
allows a borrower to repay all debt, withdraw all their collateral, and close their Cdp. Requires the borrower have a eBTC balance sufficient to repay their cdp's debt, excluding gas compensation - i.e. `(debt - 50)` eBTC. |
||
| 495 |
*/ |
||
| 496 |
function closeCdp(bytes32 _cdpId) external override {
|
||
| 497 |
⟳
|
address _borrower = sortedCdps.getOwnerAddress(_cdpId); |
|
| 498 |
⟳
|
_requireBorrowerOrPositionManagerAndUpdate(_borrower); |
|
| 499 | |||
| 500 |
⟳
|
_requireCdpisActive(cdpManager, _cdpId); |
|
| 501 | |||
| 502 |
⟳
|
cdpManager.syncAccounting(_cdpId); |
|
| 503 | |||
| 504 |
⟳
|
uint256 price = priceFeed.fetchPrice(); |
|
| 505 |
⟳
|
_requireNotInRecoveryMode(_getTCR(price)); |
|
| 506 | |||
| 507 |
⟳
|
uint256 coll = cdpManager.getCdpCollShares(_cdpId); |
|
| 508 |
⟳
|
uint256 debt = cdpManager.getCdpDebt(_cdpId); |
|
| 509 |
⟳
|
uint256 liquidatorRewardShares = cdpManager.getCdpLiquidatorRewardShares(_cdpId); |
|
| 510 | |||
| 511 |
⟳
|
_requireSufficientEbtcBalance(msg.sender, debt); |
|
| 512 | |||
| 513 |
⟳
|
uint256 newTCR = _getNewTCRFromCdpChange( |
|
| 514 |
⟳
|
collateral.getPooledEthByShares(coll), |
|
| 515 |
⟳
|
false, |
|
| 516 |
⟳
|
debt, |
|
| 517 |
⟳
|
false, |
|
| 518 |
⟳
|
price |
|
| 519 |
); |
||
| 520 |
⟳
|
_requireNewTCRisNotBelowCCR(newTCR); |
|
| 521 | |||
| 522 |
// == Grace Period == // |
||
| 523 |
// By definition we are not in RM, notify CDPManager to ensure "Glass is on" |
||
| 524 |
⟳
|
cdpManager.notifyEndGracePeriod(newTCR); |
|
| 525 | |||
| 526 |
// We already verified msg.sender is the borrower |
||
| 527 |
⟳
|
cdpManager.closeCdp(_cdpId, msg.sender, debt, coll); |
|
| 528 | |||
| 529 |
// Burn the repaid EBTC from the user's balance |
||
| 530 |
⟳
|
_repayDebt(msg.sender, debt); |
|
| 531 | |||
| 532 |
// CEI: Send the collateral and liquidator reward shares back to the user |
||
| 533 |
⟳
|
activePool.transferSystemCollSharesAndLiquidatorReward( |
|
| 534 |
⟳
|
msg.sender, |
|
| 535 |
⟳
|
coll, |
|
| 536 |
⟳
|
liquidatorRewardShares |
|
| 537 |
); |
||
| 538 |
} |
||
| 539 | |||
| 540 |
/** |
||
| 541 |
* Claim remaining collateral from a redemption or from a liquidation with ICR > MCR in Recovery Mode |
||
| 542 | |||
| 543 |
when a borrower’s Cdp has been fully redeemed from and closed, or liquidated in Recovery Mode with a collateralization ratio above 110%, this function allows the borrower to claim their stETH collateral surplus that remains in the system (collateral - debt upon redemption; collateral - 110% of the debt upon liquidation). |
||
| 544 |
*/ |
||
| 545 |
function claimSurplusCollShares() external override {
|
||
| 546 |
// send ETH from CollSurplus Pool to owner |
||
| 547 |
collSurplusPool.claimSurplusCollShares(msg.sender); |
||
| 548 |
} |
||
| 549 | |||
| 550 |
/// @notice Returns true if the borrower is allowing position manager to act on their behalf |
||
| 551 |
function getPositionManagerApproval( |
||
| 552 |
address _borrower, |
||
| 553 |
address _positionManager |
||
| 554 |
) external view override returns (PositionManagerApproval) {
|
||
| 555 |
return _getPositionManagerApproval(_borrower, _positionManager); |
||
| 556 |
} |
||
| 557 | |||
| 558 |
function _getPositionManagerApproval( |
||
| 559 |
address _borrower, |
||
| 560 |
address _positionManager |
||
| 561 |
) internal view returns (PositionManagerApproval) {
|
||
| 562 |
return positionManagers[_borrower][_positionManager]; |
||
| 563 |
} |
||
| 564 | |||
| 565 |
/// @notice Approve an account to take arbitrary actions on your Cdps. |
||
| 566 |
/// @notice Account managers with 'Persistent' status will be able to take actions indefinitely |
||
| 567 |
/// @notice Account managers with 'OneTIme' status will be able to take a single action on one Cdp. Approval will be automatically revoked after one Cdp-related action. |
||
| 568 |
/// @notice Similar to approving tokens, approving a position manager allows _stealing of all positions_ if given to a malicious account. |
||
| 569 |
function setPositionManagerApproval( |
||
| 570 |
address _positionManager, |
||
| 571 |
PositionManagerApproval _approval |
||
| 572 |
) external override {
|
||
| 573 |
_setPositionManagerApproval(msg.sender, _positionManager, _approval); |
||
| 574 |
} |
||
| 575 | |||
| 576 |
function _setPositionManagerApproval( |
||
| 577 |
address _borrower, |
||
| 578 |
address _positionManager, |
||
| 579 |
PositionManagerApproval _approval |
||
| 580 |
) internal {
|
||
| 581 |
positionManagers[_borrower][_positionManager] = _approval; |
||
| 582 |
emit PositionManagerApprovalSet(_borrower, _positionManager, _approval); |
||
| 583 |
} |
||
| 584 | |||
| 585 |
/// @notice Revoke a position manager from taking further actions on your Cdps |
||
| 586 |
/// @notice Similar to approving tokens, approving a position manager allows _stealing of all positions_ if given to a malicious account. |
||
| 587 |
function revokePositionManagerApproval(address _positionManager) external override {
|
||
| 588 |
_setPositionManagerApproval(msg.sender, _positionManager, PositionManagerApproval.None); |
||
| 589 |
} |
||
| 590 | |||
| 591 |
/// @notice Allows recipient of delegation to renounce it |
||
| 592 |
function renouncePositionManagerApproval(address _borrower) external override {
|
||
| 593 |
_setPositionManagerApproval(_borrower, msg.sender, PositionManagerApproval.None); |
||
| 594 |
} |
||
| 595 | |||
| 596 |
function DOMAIN_SEPARATOR() external view returns (bytes32) {
|
||
| 597 |
return domainSeparator(); |
||
| 598 |
} |
||
| 599 | |||
| 600 |
function domainSeparator() public view override returns (bytes32) {
|
||
| 601 |
if (_chainID() == _CACHED_CHAIN_ID) {
|
||
| 602 |
return _CACHED_DOMAIN_SEPARATOR; |
||
| 603 |
} else {
|
||
| 604 |
return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION); |
||
| 605 |
} |
||
| 606 |
} |
||
| 607 | |||
| 608 |
√
|
function _chainID() private view returns (uint256) {
|
|
| 609 |
√
|
return block.chainid; |
|
| 610 |
} |
||
| 611 | |||
| 612 |
function _buildDomainSeparator( |
||
| 613 |
bytes32 typeHash, |
||
| 614 |
bytes32 name, |
||
| 615 |
bytes32 version |
||
| 616 |
√
|
) private view returns (bytes32) {
|
|
| 617 |
√
|
return keccak256(abi.encode(typeHash, name, version, _chainID(), address(this))); |
|
| 618 |
} |
||
| 619 | |||
| 620 |
function version() external pure override returns (string memory) {
|
||
| 621 |
return _VERSION; |
||
| 622 |
} |
||
| 623 | |||
| 624 |
function permitTypeHash() external pure override returns (bytes32) {
|
||
| 625 |
return _PERMIT_POSITION_MANAGER_TYPEHASH; |
||
| 626 |
} |
||
| 627 | |||
| 628 |
function permitPositionManagerApproval( |
||
| 629 |
address _borrower, |
||
| 630 |
address _positionManager, |
||
| 631 |
PositionManagerApproval _approval, |
||
| 632 |
uint256 _deadline, |
||
| 633 |
uint8 v, |
||
| 634 |
bytes32 r, |
||
| 635 |
bytes32 s |
||
| 636 |
) external override {
|
||
| 637 |
require(_deadline >= block.timestamp, "BorrowerOperations: Position manager permit expired"); |
||
| 638 | |||
| 639 |
bytes32 digest = keccak256( |
||
| 640 |
abi.encodePacked( |
||
| 641 |
"\x19\x01", |
||
| 642 |
domainSeparator(), |
||
| 643 |
keccak256( |
||
| 644 |
abi.encode( |
||
| 645 |
_PERMIT_POSITION_MANAGER_TYPEHASH, |
||
| 646 |
_borrower, |
||
| 647 |
_positionManager, |
||
| 648 |
_approval, |
||
| 649 |
_nonces[_borrower]++, |
||
| 650 |
_deadline |
||
| 651 |
) |
||
| 652 |
) |
||
| 653 |
) |
||
| 654 |
); |
||
| 655 |
address recoveredAddress = ecrecover(digest, v, r, s); |
||
| 656 |
require( |
||
| 657 |
recoveredAddress != address(0) && recoveredAddress == _borrower, |
||
| 658 |
"BorrowerOperations: Invalid signature" |
||
| 659 |
); |
||
| 660 | |||
| 661 |
_setPositionManagerApproval(_borrower, _positionManager, _approval); |
||
| 662 |
} |
||
| 663 | |||
| 664 |
// --- Helper functions --- |
||
| 665 | |||
| 666 |
function _getCollSharesChangeFromStEthChange( |
||
| 667 |
uint256 _collReceived, |
||
| 668 |
uint256 _requestedCollWithdrawal |
||
| 669 |
√
|
⟳
|
) internal view returns (uint256 collChange, bool isCollIncrease) {
|
| 670 |
√
|
⟳
|
if (_collReceived != 0) {
|
| 671 |
√
|
collChange = collateral.getSharesByPooledEth(_collReceived); |
|
| 672 |
√
|
isCollIncrease = true; |
|
| 673 |
} else {
|
||
| 674 |
√
|
⟳
|
collChange = collateral.getSharesByPooledEth(_requestedCollWithdrawal); |
| 675 |
} |
||
| 676 |
} |
||
| 677 | |||
| 678 |
/** |
||
| 679 |
@notice Process the token movements required by a CDP adjustment. |
||
| 680 |
@notice Handles the cases of a debt increase / decrease, and/or a collateral increase / decrease. |
||
| 681 |
*/ |
||
| 682 |
function _processTokenMovesFromAdjustment( |
||
| 683 |
LocalVariables_moveTokens memory _varMvTokens |
||
| 684 |
) internal {
|
||
| 685 |
// Debt increase: mint change value of new eBTC to user, increment ActivePool eBTC internal accounting |
||
| 686 |
√
|
if (_varMvTokens.isDebtIncrease) {
|
|
| 687 |
√
|
_withdrawDebt(_varMvTokens.user, _varMvTokens.EBTCChange, _varMvTokens.netDebtChange); |
|
| 688 |
} else {
|
||
| 689 |
// Debt decrease: burn change value of eBTC from user, decrement ActivePool eBTC internal accounting |
||
| 690 |
√
|
_repayDebt(_varMvTokens.user, _varMvTokens.EBTCChange); |
|
| 691 |
} |
||
| 692 | |||
| 693 |
√
|
if (_varMvTokens.isCollIncrease) {
|
|
| 694 |
// Coll increase: send change value of stETH to Active Pool, increment ActivePool stETH internal accounting |
||
| 695 |
√
|
_activePoolAddColl(_varMvTokens.collAddUnderlying, _varMvTokens.collChange); |
|
| 696 |
} else {
|
||
| 697 |
// Coll decrease: send change value of stETH to user, decrement ActivePool stETH internal accounting |
||
| 698 |
√
|
activePool.transferSystemCollShares(_varMvTokens.user, _varMvTokens.collChange); |
|
| 699 |
} |
||
| 700 |
} |
||
| 701 | |||
| 702 |
/// @notice Send stETH to Active Pool and increase its recorded ETH balance |
||
| 703 |
/// @param _stEthBalance total balance of stETH to send, inclusive of coll and liquidatorRewardShares |
||
| 704 |
/// @param _sharesToTrack coll as shares (exclsuive of liquidator reward shares) |
||
| 705 |
/// @dev Liquidator reward shares are not considered as part of the system for CR purposes. |
||
| 706 |
/// @dev These number of liquidator shares associated with each CDP are stored in the CDP, while the actual tokens float in the active pool |
||
| 707 |
function _activePoolAddColl(uint256 _stEthBalance, uint256 _sharesToTrack) internal {
|
||
| 708 |
// NOTE: No need for safe transfer if the collateral asset is standard. Make sure this is the case! |
||
| 709 |
√
|
collateral.transferFrom(msg.sender, address(activePool), _stEthBalance); |
|
| 710 |
√
|
activePool.increaseSystemCollShares(_sharesToTrack); |
|
| 711 |
} |
||
| 712 | |||
| 713 |
/// @dev Mint specified debt tokens to account and change global debt accounting accordingly |
||
| 714 |
function _withdrawDebt(address _account, uint256 _debt, uint256 _netDebtIncrease) internal {
|
||
| 715 |
√
|
activePool.increaseSystemDebt(_netDebtIncrease); |
|
| 716 |
√
|
ebtcToken.mint(_account, _debt); |
|
| 717 |
} |
||
| 718 | |||
| 719 |
// Burn the specified amount of EBTC from _account and decreases the total active debt |
||
| 720 |
function _repayDebt(address _account, uint256 _debt) internal {
|
||
| 721 |
√
|
⟳
|
activePool.decreaseSystemDebt(_debt); |
| 722 |
√
|
⟳
|
ebtcToken.burn(_account, _debt); |
| 723 |
} |
||
| 724 | |||
| 725 |
// --- 'Require' wrapper functions --- |
||
| 726 | |||
| 727 |
function _requireSingularCollChange( |
||
| 728 |
uint256 _stEthBalanceIncrease, |
||
| 729 |
uint256 _stEthBalanceDecrease |
||
| 730 |
) internal pure {
|
||
| 731 |
require( |
||
| 732 |
√
|
⟳
|
_stEthBalanceIncrease == 0 || _stEthBalanceDecrease == 0, |
| 733 |
"BorrowerOperations: Cannot add and withdraw collateral in same operation" |
||
| 734 |
); |
||
| 735 |
} |
||
| 736 | |||
| 737 |
function _requireNonZeroAdjustment( |
||
| 738 |
uint256 _stEthBalanceIncrease, |
||
| 739 |
uint256 _debtChange, |
||
| 740 |
uint256 _stEthBalanceDecrease |
||
| 741 |
) internal pure {
|
||
| 742 |
require( |
||
| 743 |
√
|
⟳
|
_stEthBalanceIncrease != 0 || _stEthBalanceDecrease != 0 || _debtChange != 0, |
| 744 |
"BorrowerOperations: There must be either a collateral change or a debt change" |
||
| 745 |
); |
||
| 746 |
} |
||
| 747 | |||
| 748 |
function _requireCdpisActive(ICdpManager _cdpManager, bytes32 _cdpId) internal view {
|
||
| 749 |
√
|
⟳
|
uint256 status = _cdpManager.getCdpStatus(_cdpId); |
| 750 |
√
|
⟳
|
require(status == 1, "BorrowerOperations: Cdp does not exist or is closed"); |
| 751 |
} |
||
| 752 | |||
| 753 |
function _requireCdpIsNonExistent(bytes32 _cdpId) internal view {
|
||
| 754 |
√
|
uint status = cdpManager.getCdpStatus(_cdpId); |
|
| 755 |
√
|
require(status == 0, "BorrowerOperations: Cdp is active or has been previously closed"); |
|
| 756 |
} |
||
| 757 | |||
| 758 |
function _requireNonZeroDebtChange(uint _debtChange) internal pure {
|
||
| 759 |
√
|
⟳
|
require(_debtChange > 0, "BorrowerOperations: Debt increase requires non-zero debtChange"); |
| 760 |
} |
||
| 761 | |||
| 762 |
function _requireNotInRecoveryMode(uint256 _tcr) internal view {
|
||
| 763 |
require( |
||
| 764 |
⟳
|
!_checkRecoveryModeForTCR(_tcr), |
|
| 765 |
"BorrowerOperations: Operation not permitted during Recovery Mode" |
||
| 766 |
); |
||
| 767 |
} |
||
| 768 | |||
| 769 |
function _requireNoStEthBalanceDecrease(uint256 _stEthBalanceDecrease) internal pure {
|
||
| 770 |
require( |
||
| 771 |
_stEthBalanceDecrease == 0, |
||
| 772 |
"BorrowerOperations: Collateral withdrawal not permitted during Recovery Mode" |
||
| 773 |
); |
||
| 774 |
} |
||
| 775 | |||
| 776 |
function _requireValidAdjustmentInCurrentMode( |
||
| 777 |
bool _isRecoveryMode, |
||
| 778 |
uint256 _stEthBalanceDecrease, |
||
| 779 |
bool _isDebtIncrease, |
||
| 780 |
LocalVariables_adjustCdp memory _vars |
||
| 781 |
) internal {
|
||
| 782 |
/* |
||
| 783 |
*In Recovery Mode, only allow: |
||
| 784 |
* |
||
| 785 |
* - Pure collateral top-up |
||
| 786 |
* - Pure debt repayment |
||
| 787 |
* - Collateral top-up with debt repayment |
||
| 788 |
* - A debt increase combined with a collateral top-up which makes the |
||
| 789 |
* ICR >= 150% and improves the ICR (and by extension improves the TCR). |
||
| 790 |
* |
||
| 791 |
* In Normal Mode, ensure: |
||
| 792 |
* |
||
| 793 |
* - The new ICR is above MCR |
||
| 794 |
* - The adjustment won't pull the TCR below CCR |
||
| 795 |
*/ |
||
| 796 | |||
| 797 |
√
|
⟳
|
_vars.newTCR = _getNewTCRFromCdpChange( |
| 798 |
√
|
⟳
|
collateral.getPooledEthByShares(_vars.collChange), |
| 799 |
√
|
⟳
|
_vars.isCollIncrease, |
| 800 |
√
|
⟳
|
_vars.netDebtChange, |
| 801 |
√
|
⟳
|
_isDebtIncrease, |
| 802 |
√
|
⟳
|
_vars.price |
| 803 |
); |
||
| 804 | |||
| 805 |
√
|
⟳
|
if (_isRecoveryMode) {
|
| 806 |
_requireNoStEthBalanceDecrease(_stEthBalanceDecrease); |
||
| 807 |
if (_isDebtIncrease) {
|
||
| 808 |
_requireICRisNotBelowCCR(_vars.newICR); |
||
| 809 |
_requireNoDecreaseOfICR(_vars.newICR, _vars.oldICR); |
||
| 810 |
} |
||
| 811 | |||
| 812 |
// == Grace Period == // |
||
| 813 |
// We are in RM, Edge case is Depositing Coll could exit RM |
||
| 814 |
// We check with newTCR |
||
| 815 |
if (_vars.newTCR < CCR) {
|
||
| 816 |
// Notify RM |
||
| 817 |
cdpManager.notifyStartGracePeriod(_vars.newTCR); |
||
| 818 |
} else {
|
||
| 819 |
// Notify Back to Normal Mode |
||
| 820 |
cdpManager.notifyEndGracePeriod(_vars.newTCR); |
||
| 821 |
} |
||
| 822 |
} else {
|
||
| 823 |
// if Normal Mode |
||
| 824 |
√
|
⟳
|
_requireICRisNotBelowMCR(_vars.newICR); |
| 825 |
√
|
⟳
|
_requireNewTCRisNotBelowCCR(_vars.newTCR); |
| 826 | |||
| 827 |
// == Grace Period == // |
||
| 828 |
// We are not in RM, no edge case, we always stay above RM |
||
| 829 |
// Always Notify Back to Normal Mode |
||
| 830 |
√
|
⟳
|
cdpManager.notifyEndGracePeriod(_vars.newTCR); |
| 831 |
} |
||
| 832 |
} |
||
| 833 | |||
| 834 |
function _requireICRisNotBelowMCR(uint256 _newICR) internal pure {
|
||
| 835 |
require( |
||
| 836 |
√
|
⟳
|
_newICR >= MCR, |
| 837 |
"BorrowerOperations: An operation that would result in ICR < MCR is not permitted" |
||
| 838 |
); |
||
| 839 |
} |
||
| 840 | |||
| 841 |
function _requireICRisNotBelowCCR(uint256 _newICR) internal pure {
|
||
| 842 |
require(_newICR >= CCR, "BorrowerOperations: Operation must leave cdp with ICR >= CCR"); |
||
| 843 |
} |
||
| 844 | |||
| 845 |
function _requireNoDecreaseOfICR(uint256 _newICR, uint256 _oldICR) internal pure {
|
||
| 846 |
require( |
||
| 847 |
_newICR >= _oldICR, |
||
| 848 |
"BorrowerOperations: Cannot decrease your Cdp's ICR in Recovery Mode" |
||
| 849 |
); |
||
| 850 |
} |
||
| 851 | |||
| 852 |
function _requireNewTCRisNotBelowCCR(uint256 _newTCR) internal pure {
|
||
| 853 |
require( |
||
| 854 |
√
|
⟳
|
_newTCR >= CCR, |
| 855 |
"BorrowerOperations: An operation that would result in TCR < CCR is not permitted" |
||
| 856 |
); |
||
| 857 |
} |
||
| 858 | |||
| 859 |
function _requireNonZeroDebt(uint256 _debt) internal pure {
|
||
| 860 |
√
|
⟳
|
require(_debt > 0, "BorrowerOperations: Debt must be non-zero"); |
| 861 |
} |
||
| 862 | |||
| 863 |
function _requireAtLeastMinNetStEthBalance(uint256 _coll) internal pure {
|
||
| 864 |
require( |
||
| 865 |
√
|
⟳
|
_coll >= MIN_NET_COLL, |
| 866 |
"BorrowerOperations: Cdp's net coll must not fall below minimum" |
||
| 867 |
); |
||
| 868 |
} |
||
| 869 | |||
| 870 |
function _requireValidDebtRepayment(uint256 _currentDebt, uint256 _debtRepayment) internal pure {
|
||
| 871 |
require( |
||
| 872 |
√
|
⟳
|
_debtRepayment <= _currentDebt, |
| 873 |
"BorrowerOperations: Amount repaid must not be larger than the Cdp's debt" |
||
| 874 |
); |
||
| 875 |
} |
||
| 876 | |||
| 877 |
function _requireSufficientEbtcBalance(address _account, uint256 _debtRepayment) internal view {
|
||
| 878 |
require( |
||
| 879 |
√
|
⟳
|
ebtcToken.balanceOf(_account) >= _debtRepayment, |
| 880 |
"BorrowerOperations: Caller doesnt have enough eBTC to make repayment" |
||
| 881 |
); |
||
| 882 |
} |
||
| 883 | |||
| 884 |
function _requireBorrowerOrPositionManagerAndUpdate(address _borrower) internal {
|
||
| 885 |
√
|
⟳
|
if (_borrower == msg.sender) {
|
| 886 |
√
|
⟳
|
return; // Early return, no delegation |
| 887 |
} |
||
| 888 | |||
| 889 |
PositionManagerApproval _approval = _getPositionManagerApproval(_borrower, msg.sender); |
||
| 890 |
// Must be an approved position manager at this point |
||
| 891 |
require( |
||
| 892 |
_approval != PositionManagerApproval.None, |
||
| 893 |
"BorrowerOperations: Only borrower account or approved position manager can OpenCdp on borrower's behalf" |
||
| 894 |
); |
||
| 895 | |||
| 896 |
// Conditional Adjustment |
||
| 897 |
/// @dev If this is a position manager operation with a one-time approval, clear that approval |
||
| 898 |
/// @dev If the PositionManagerApproval was none, we should have failed with the check in _requireBorrowerOrPositionManagerAndUpdate |
||
| 899 |
if (_approval == PositionManagerApproval.OneTime) {
|
||
| 900 |
_setPositionManagerApproval(_borrower, msg.sender, PositionManagerApproval.None); |
||
| 901 |
} |
||
| 902 |
} |
||
| 903 | |||
| 904 |
// --- ICR and TCR getters --- |
||
| 905 | |||
| 906 |
// Compute the new collateral ratio, considering the change in coll and debt. Assumes 0 pending rewards. |
||
| 907 |
function _getNewNominalICRFromCdpChange( |
||
| 908 |
LocalVariables_adjustCdp memory vars, |
||
| 909 |
bool _isDebtIncrease |
||
| 910 |
√
|
) internal pure returns (uint256) {
|
|
| 911 |
√
|
(uint256 newColl, uint256 newDebt) = _getNewCdpAmounts( |
|
| 912 |
√
|
vars.coll, |
|
| 913 |
√
|
vars.debt, |
|
| 914 |
√
|
vars.collChange, |
|
| 915 |
√
|
vars.isCollIncrease, |
|
| 916 |
√
|
vars.netDebtChange, |
|
| 917 |
√
|
_isDebtIncrease |
|
| 918 |
); |
||
| 919 | |||
| 920 |
√
|
uint256 newNICR = EbtcMath._computeNominalCR(newColl, newDebt); |
|
| 921 |
√
|
return newNICR; |
|
| 922 |
} |
||
| 923 | |||
| 924 |
// Compute the new collateral ratio, considering the change in coll and debt. Assumes 0 pending rewards. |
||
| 925 |
function _getNewICRFromCdpChange( |
||
| 926 |
uint256 _coll, |
||
| 927 |
uint256 _debt, |
||
| 928 |
uint256 _collChange, |
||
| 929 |
bool _isCollIncrease, |
||
| 930 |
uint256 _debtChange, |
||
| 931 |
bool _isDebtIncrease, |
||
| 932 |
uint256 _price |
||
| 933 |
√
|
⟳
|
) internal view returns (uint256) {
|
| 934 |
√
|
⟳
|
(uint256 newColl, uint256 newDebt) = _getNewCdpAmounts( |
| 935 |
√
|
⟳
|
_coll, |
| 936 |
√
|
⟳
|
_debt, |
| 937 |
√
|
⟳
|
_collChange, |
| 938 |
√
|
⟳
|
_isCollIncrease, |
| 939 |
√
|
⟳
|
_debtChange, |
| 940 |
√
|
⟳
|
_isDebtIncrease |
| 941 |
); |
||
| 942 | |||
| 943 |
√
|
⟳
|
uint256 newICR = EbtcMath._computeCR( |
| 944 |
√
|
⟳
|
collateral.getPooledEthByShares(newColl), |
| 945 |
√
|
⟳
|
newDebt, |
| 946 |
√
|
⟳
|
_price |
| 947 |
); |
||
| 948 |
√
|
⟳
|
return newICR; |
| 949 |
} |
||
| 950 | |||
| 951 |
function _getNewCdpAmounts( |
||
| 952 |
uint256 _coll, |
||
| 953 |
uint256 _debt, |
||
| 954 |
uint256 _collChange, |
||
| 955 |
bool _isCollIncrease, |
||
| 956 |
uint256 _debtChange, |
||
| 957 |
bool _isDebtIncrease |
||
| 958 |
√
|
⟳
|
) internal pure returns (uint256, uint256) {
|
| 959 |
√
|
⟳
|
uint256 newColl = _coll; |
| 960 |
√
|
⟳
|
uint256 newDebt = _debt; |
| 961 | |||
| 962 |
√
|
⟳
|
newColl = _isCollIncrease ? _coll + _collChange : _coll - _collChange; |
| 963 |
√
|
⟳
|
newDebt = _isDebtIncrease ? _debt + _debtChange : _debt - _debtChange; |
| 964 | |||
| 965 |
√
|
⟳
|
return (newColl, newDebt); |
| 966 |
} |
||
| 967 | |||
| 968 |
function _getNewTCRFromCdpChange( |
||
| 969 |
uint256 _collChange, |
||
| 970 |
bool _isCollIncrease, |
||
| 971 |
uint256 _debtChange, |
||
| 972 |
bool _isDebtIncrease, |
||
| 973 |
uint256 _price |
||
| 974 |
√
|
⟳
|
) internal view returns (uint256) {
|
| 975 |
√
|
⟳
|
uint256 _shareColl = getSystemCollShares(); |
| 976 |
√
|
⟳
|
uint256 totalColl = collateral.getPooledEthByShares(_shareColl); |
| 977 |
√
|
⟳
|
uint256 totalDebt = _getSystemDebt(); |
| 978 | |||
| 979 |
√
|
⟳
|
totalColl = _isCollIncrease ? totalColl + _collChange : totalColl - _collChange; |
| 980 |
√
|
⟳
|
totalDebt = _isDebtIncrease ? totalDebt + _debtChange : totalDebt - _debtChange; |
| 981 | |||
| 982 |
√
|
⟳
|
uint256 newTCR = EbtcMath._computeCR(totalColl, totalDebt, _price); |
| 983 |
√
|
⟳
|
return newTCR; |
| 984 |
} |
||
| 985 | |||
| 986 |
// === Flash Loans === // |
||
| 987 |
function flashLoan( |
||
| 988 |
IERC3156FlashBorrower receiver, |
||
| 989 |
address token, |
||
| 990 |
uint256 amount, |
||
| 991 |
bytes calldata data |
||
| 992 |
) external override returns (bool) {
|
||
| 993 |
require(amount > 0, "BorrowerOperations: 0 Amount"); |
||
| 994 |
uint256 fee = flashFee(token, amount); // NOTE: Check for `eBTCToken` is implicit here // NOTE: Pause check is here |
||
| 995 |
require(amount <= maxFlashLoan(token), "BorrowerOperations: Too much"); |
||
| 996 | |||
| 997 |
// Issue EBTC |
||
| 998 |
ebtcToken.mint(address(receiver), amount); |
||
| 999 | |||
| 1000 |
// Callback |
||
| 1001 |
require( |
||
| 1002 |
receiver.onFlashLoan(msg.sender, token, amount, fee, data) == FLASH_SUCCESS_VALUE, |
||
| 1003 |
"IERC3156: Callback failed" |
||
| 1004 |
); |
||
| 1005 | |||
| 1006 |
// Gas: Repay from user balance, so we don't trigger a new SSTORE |
||
| 1007 |
// Safe to use transferFrom and unchecked as it's a standard token |
||
| 1008 |
// Also saves gas |
||
| 1009 |
// Send both fee and amount to FEE_RECIPIENT, to burn allowance per EIP-3156 |
||
| 1010 |
ebtcToken.transferFrom(address(receiver), feeRecipientAddress, fee + amount); |
||
| 1011 | |||
| 1012 |
// Burn amount, from FEE_RECIPIENT |
||
| 1013 |
ebtcToken.burn(feeRecipientAddress, amount); |
||
| 1014 | |||
| 1015 |
emit FlashLoanSuccess(address(receiver), token, amount, fee); |
||
| 1016 | |||
| 1017 |
return true; |
||
| 1018 |
} |
||
| 1019 | |||
| 1020 |
function flashFee(address token, uint256 amount) public view override returns (uint256) {
|
||
| 1021 |
require(token == address(ebtcToken), "BorrowerOperations: EBTC Only"); |
||
| 1022 |
require(!flashLoansPaused, "BorrowerOperations: Flash Loans Paused"); |
||
| 1023 | |||
| 1024 |
return (amount * feeBps) / MAX_BPS; |
||
| 1025 |
} |
||
| 1026 | |||
| 1027 |
/// @dev Max flashloan, exclusively in ETH equals to the current balance |
||
| 1028 |
function maxFlashLoan(address token) public view override returns (uint256) {
|
||
| 1029 |
if (token != address(ebtcToken)) {
|
||
| 1030 |
return 0; |
||
| 1031 |
} |
||
| 1032 | |||
| 1033 |
if (flashLoansPaused) {
|
||
| 1034 |
return 0; |
||
| 1035 |
} |
||
| 1036 | |||
| 1037 |
return type(uint112).max; |
||
| 1038 |
} |
||
| 1039 | |||
| 1040 |
// === Governed Functions == |
||
| 1041 | |||
| 1042 |
function setFeeRecipientAddress(address _feeRecipientAddress) external requiresAuth {
|
||
| 1043 |
require( |
||
| 1044 |
_feeRecipientAddress != address(0), |
||
| 1045 |
"BorrowerOperations: Cannot set feeRecipient to zero address" |
||
| 1046 |
); |
||
| 1047 | |||
| 1048 |
cdpManager.syncGlobalAccounting(); |
||
| 1049 | |||
| 1050 |
feeRecipientAddress = _feeRecipientAddress; |
||
| 1051 |
emit FeeRecipientAddressChanged(_feeRecipientAddress); |
||
| 1052 |
} |
||
| 1053 | |||
| 1054 |
function setFeeBps(uint256 _newFee) external requiresAuth {
|
||
| 1055 |
require(_newFee <= MAX_FEE_BPS, "ERC3156FlashLender: _newFee should <= MAX_FEE_BPS"); |
||
| 1056 | |||
| 1057 |
cdpManager.syncGlobalAccounting(); |
||
| 1058 | |||
| 1059 |
// set new flash fee |
||
| 1060 |
uint256 _oldFee = feeBps; |
||
| 1061 |
feeBps = uint16(_newFee); |
||
| 1062 |
emit FlashFeeSet(msg.sender, _oldFee, _newFee); |
||
| 1063 |
} |
||
| 1064 | |||
| 1065 |
function setFlashLoansPaused(bool _paused) external requiresAuth {
|
||
| 1066 |
cdpManager.syncGlobalAccounting(); |
||
| 1067 | |||
| 1068 |
flashLoansPaused = _paused; |
||
| 1069 |
emit FlashLoansPaused(msg.sender, _paused); |
||
| 1070 |
} |
||
| 1071 |
} |
||
| 1072 |
| Lines covered: | 52 / 61 (85.2%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Interfaces/IPriceFeed.sol"; |
||
| 6 |
import "./Interfaces/ICdpManager.sol"; |
||
| 7 | |||
| 8 |
/// @notice The contract allows to check real CR of CDPs |
||
| 9 |
/// Acknowledgement: https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/Quoter.sol |
||
| 10 |
contract CRLens {
|
||
| 11 |
ICdpManager public immutable cdpManager; |
||
| 12 |
IPriceFeed public immutable priceFeed; |
||
| 13 | |||
| 14 |
constructor(address _cdpManager, address _priceFeed) {
|
||
| 15 |
√
|
cdpManager = ICdpManager(_cdpManager); |
|
| 16 |
√
|
priceFeed = IPriceFeed(_priceFeed); |
|
| 17 |
} |
||
| 18 | |||
| 19 |
// == CORE FUNCTIONS == // |
||
| 20 | |||
| 21 |
/// @notice Returns the TCR of the system after the fee split |
||
| 22 |
/// @dev Call this from offChain with `eth_call` to avoid paying for gas |
||
| 23 |
⟳
|
function getRealTCR(bool revertValue) external returns (uint256) {
|
|
| 24 |
// Synch State |
||
| 25 |
⟳
|
cdpManager.syncGlobalAccountingAndGracePeriod(); |
|
| 26 | |||
| 27 |
// Return latest |
||
| 28 |
⟳
|
uint price = priceFeed.fetchPrice(); |
|
| 29 |
⟳
|
uint256 tcr = cdpManager.getTCR(price); |
|
| 30 | |||
| 31 |
⟳
|
if (revertValue) {
|
|
| 32 |
assembly {
|
||
| 33 |
⟳
|
let ptr := mload(0x40) |
|
| 34 |
⟳
|
mstore(ptr, tcr) |
|
| 35 |
⟳
|
revert(ptr, 32) |
|
| 36 |
} |
||
| 37 |
} |
||
| 38 | |||
| 39 |
return tcr; |
||
| 40 |
} |
||
| 41 | |||
| 42 |
/// @notice Return the ICR of a CDP after the fee split |
||
| 43 |
/// @dev Call this from offChain with `eth_call` to avoid paying for gas |
||
| 44 |
⟳
|
function getRealICR(bytes32 cdpId, bool revertValue) external returns (uint256) {
|
|
| 45 |
⟳
|
cdpManager.syncAccounting(cdpId); |
|
| 46 |
⟳
|
uint price = priceFeed.fetchPrice(); |
|
| 47 |
⟳
|
uint256 icr = cdpManager.getICR(cdpId, price); |
|
| 48 | |||
| 49 |
⟳
|
if (revertValue) {
|
|
| 50 |
assembly {
|
||
| 51 |
⟳
|
let ptr := mload(0x40) |
|
| 52 |
⟳
|
mstore(ptr, icr) |
|
| 53 |
⟳
|
revert(ptr, 32) |
|
| 54 |
} |
||
| 55 |
} |
||
| 56 | |||
| 57 |
return icr; |
||
| 58 |
} |
||
| 59 | |||
| 60 |
/// @notice Return the ICR of a CDP after the fee split |
||
| 61 |
/// @dev Call this from offChain with `eth_call` to avoid paying for gas |
||
| 62 |
⟳
|
function getRealNICR(bytes32 cdpId, bool revertValue) external returns (uint256) {
|
|
| 63 |
⟳
|
cdpManager.syncAccounting(cdpId); |
|
| 64 |
⟳
|
uint price = priceFeed.fetchPrice(); |
|
| 65 |
⟳
|
uint256 icr = cdpManager.getNominalICR(cdpId); |
|
| 66 | |||
| 67 |
⟳
|
if (revertValue) {
|
|
| 68 |
assembly {
|
||
| 69 |
⟳
|
let ptr := mload(0x40) |
|
| 70 |
⟳
|
mstore(ptr, icr) |
|
| 71 |
⟳
|
revert(ptr, 32) |
|
| 72 |
} |
||
| 73 |
} |
||
| 74 | |||
| 75 |
return icr; |
||
| 76 |
} |
||
| 77 | |||
| 78 |
/// @dev Returns 1 if we're in RM |
||
| 79 |
⟳
|
function getCheckRecoveryMode(bool revertValue) external returns (uint256) {
|
|
| 80 |
// Synch State |
||
| 81 |
⟳
|
cdpManager.syncGlobalAccountingAndGracePeriod(); |
|
| 82 | |||
| 83 |
// Return latest |
||
| 84 |
⟳
|
uint price = priceFeed.fetchPrice(); |
|
| 85 |
⟳
|
uint256 isRm = cdpManager.checkRecoveryMode(price) == true ? 1 : 0; |
|
| 86 | |||
| 87 |
⟳
|
if (revertValue) {
|
|
| 88 |
assembly {
|
||
| 89 |
⟳
|
let ptr := mload(0x40) |
|
| 90 |
⟳
|
mstore(ptr, isRm) |
|
| 91 |
⟳
|
revert(ptr, 32) |
|
| 92 |
} |
||
| 93 |
} |
||
| 94 | |||
| 95 |
return isRm; |
||
| 96 |
} |
||
| 97 | |||
| 98 |
// == REVERT LOGIC == // |
||
| 99 |
// Thanks to: https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/Quoter.sol |
||
| 100 |
// NOTE: You should never use these in prod, these are just for testing // |
||
| 101 | |||
| 102 |
√
|
⟳
|
function parseRevertReason(bytes memory reason) private pure returns (uint256) {
|
| 103 |
√
|
⟳
|
if (reason.length != 32) {
|
| 104 |
if (reason.length < 68) revert("Unexpected error");
|
||
| 105 |
assembly {
|
||
| 106 |
reason := add(reason, 0x04) |
||
| 107 |
} |
||
| 108 |
revert(abi.decode(reason, (string))); |
||
| 109 |
} |
||
| 110 |
√
|
⟳
|
return abi.decode(reason, (uint256)); |
| 111 |
} |
||
| 112 | |||
| 113 |
/// @notice Returns the TCR of the system after the fee split |
||
| 114 |
/// @dev Call this from offChain with `eth_call` to avoid paying for gas |
||
| 115 |
/// These cost more gas, there should never be a reason for you to use them beside integration with Echidna |
||
| 116 |
√
|
⟳
|
function quoteRealTCR() external returns (uint256) {
|
| 117 |
√
|
⟳
|
try this.getRealTCR(true) {} catch (bytes memory reason) {
|
| 118 |
√
|
⟳
|
return parseRevertReason(reason); |
| 119 |
} |
||
| 120 |
} |
||
| 121 | |||
| 122 |
/// @notice Returns the ICR of the system after the fee split |
||
| 123 |
/// @dev Call this from offChain with `eth_call` to avoid paying for gas |
||
| 124 |
/// These cost more gas, there should never be a reason for you to use them beside integration with Echidna |
||
| 125 |
√
|
⟳
|
function quoteRealICR(bytes32 cdpId) external returns (uint256) {
|
| 126 |
√
|
⟳
|
try this.getRealICR(cdpId, true) {} catch (bytes memory reason) {
|
| 127 |
√
|
⟳
|
return parseRevertReason(reason); |
| 128 |
} |
||
| 129 |
} |
||
| 130 | |||
| 131 |
/// @notice Returns the NICR of the system after the fee split |
||
| 132 |
/// @dev Call this from offChain with `eth_call` to avoid paying for gas |
||
| 133 |
/// These cost more gas, there should never be a reason for you to use them beside integration with Echidna |
||
| 134 |
√
|
⟳
|
function quoteRealNICR(bytes32 cdpId) external returns (uint256) {
|
| 135 |
√
|
⟳
|
try this.getRealNICR(cdpId, true) {} catch (bytes memory reason) {
|
| 136 |
√
|
⟳
|
return parseRevertReason(reason); |
| 137 |
} |
||
| 138 |
} |
||
| 139 | |||
| 140 |
/// @notice Returns whether the system is in RM after taking fee split |
||
| 141 |
/// @dev Call this from offChain with `eth_call` to avoid paying for gas |
||
| 142 |
/// These cost more gas, there should never be a reason for you to use them beside integration with Echidna |
||
| 143 |
√
|
⟳
|
function quoteCheckRecoveryMode() external returns (uint256) {
|
| 144 |
√
|
⟳
|
try this.getCheckRecoveryMode(true) {} catch (bytes memory reason) {
|
| 145 |
√
|
⟳
|
return parseRevertReason(reason); |
| 146 |
} |
||
| 147 |
} |
||
| 148 | |||
| 149 |
√
|
function quoteAnything(function() external anything ) external returns(uint256) {
|
|
| 150 |
√
|
try anything() {} catch (bytes memory reason) {
|
|
| 151 |
√
|
return parseRevertReason(reason); |
|
| 152 |
} |
||
| 153 |
} |
||
| 154 |
} |
||
| 155 |
| Lines covered: | 265 / 329 (80.5%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Interfaces/ICdpManager.sol"; |
||
| 6 |
import "./Interfaces/ICollSurplusPool.sol"; |
||
| 7 |
import "./Interfaces/IEBTCToken.sol"; |
||
| 8 |
import "./Interfaces/ISortedCdps.sol"; |
||
| 9 |
import "./Dependencies/ICollateralTokenOracle.sol"; |
||
| 10 |
import "./CdpManagerStorage.sol"; |
||
| 11 |
import "./EBTCDeployer.sol"; |
||
| 12 |
import "./Dependencies/Proxy.sol"; |
||
| 13 | |||
| 14 |
contract CdpManager is CdpManagerStorage, ICdpManager, Proxy {
|
||
| 15 |
// --- Dependency setter --- |
||
| 16 | |||
| 17 |
/** |
||
| 18 |
* @notice Constructor for CdpManager contract. |
||
| 19 |
* @dev Sets up dependencies and initial staking reward split. |
||
| 20 |
* @param _liquidationLibraryAddress Address of the liquidation library. |
||
| 21 |
* @param _authorityAddress Address of the authority. |
||
| 22 |
* @param _borrowerOperationsAddress Address of BorrowerOperations. |
||
| 23 |
* @param _collSurplusPoolAddress Address of CollSurplusPool. |
||
| 24 |
* @param _ebtcTokenAddress Address of the eBTC token. |
||
| 25 |
* @param _sortedCdpsAddress Address of the SortedCDPs. |
||
| 26 |
* @param _activePoolAddress Address of the ActivePool. |
||
| 27 |
* @param _priceFeedAddress Address of the price feed. |
||
| 28 |
* @param _collTokenAddress Address of the collateral token. |
||
| 29 |
*/ |
||
| 30 |
constructor( |
||
| 31 |
address _liquidationLibraryAddress, |
||
| 32 |
address _authorityAddress, |
||
| 33 |
address _borrowerOperationsAddress, |
||
| 34 |
address _collSurplusPoolAddress, |
||
| 35 |
address _ebtcTokenAddress, |
||
| 36 |
address _sortedCdpsAddress, |
||
| 37 |
address _activePoolAddress, |
||
| 38 |
address _priceFeedAddress, |
||
| 39 |
address _collTokenAddress |
||
| 40 |
) |
||
| 41 |
CdpManagerStorage( |
||
| 42 |
√
|
_liquidationLibraryAddress, |
|
| 43 |
√
|
_authorityAddress, |
|
| 44 |
√
|
_borrowerOperationsAddress, |
|
| 45 |
√
|
_collSurplusPoolAddress, |
|
| 46 |
√
|
_ebtcTokenAddress, |
|
| 47 |
√
|
_sortedCdpsAddress, |
|
| 48 |
√
|
_activePoolAddress, |
|
| 49 |
√
|
_priceFeedAddress, |
|
| 50 |
√
|
_collTokenAddress |
|
| 51 |
) |
||
| 52 |
{
|
||
| 53 |
√
|
stakingRewardSplit = STAKING_REWARD_SPLIT; |
|
| 54 |
// Emit initial value for analytics |
||
| 55 |
√
|
emit StakingRewardSplitSet(stakingRewardSplit); |
|
| 56 | |||
| 57 |
√
|
(uint256 _oldIndex, uint256 _newIndex) = _readStEthIndex(); |
|
| 58 |
√
|
_syncStEthIndex(_oldIndex, _newIndex); |
|
| 59 |
√
|
systemStEthFeePerUnitIndex = DECIMAL_PRECISION; |
|
| 60 |
} |
||
| 61 | |||
| 62 |
// --- Getters --- |
||
| 63 | |||
| 64 |
/** |
||
| 65 |
* @notice Get the count of CDPs in the system |
||
| 66 |
* @return The number of CDPs. |
||
| 67 |
*/ |
||
| 68 | |||
| 69 |
√
|
⟳
|
function getActiveCdpsCount() external view override returns (uint256) {
|
| 70 |
√
|
⟳
|
return CdpIds.length; |
| 71 |
} |
||
| 72 | |||
| 73 |
/** |
||
| 74 |
* @notice Get the CdpId at a given index in the CdpIds array. |
||
| 75 |
* @param _index Index of the CdpIds array. |
||
| 76 |
* @return CDP ID. |
||
| 77 |
*/ |
||
| 78 |
function getIdFromCdpIdsArray(uint256 _index) external view override returns (bytes32) {
|
||
| 79 |
return CdpIds[_index]; |
||
| 80 |
} |
||
| 81 | |||
| 82 |
// --- Cdp Liquidation functions --- |
||
| 83 |
// ----------------------------------------------------------------- |
||
| 84 |
// CDP ICR | Liquidation Behavior (TODO gas compensation?) |
||
| 85 |
// |
||
| 86 |
// < MCR | debt could be fully repaid by liquidator |
||
| 87 |
// | and ALL collateral transferred to liquidator |
||
| 88 |
// | OR debt could be partially repaid by liquidator and |
||
| 89 |
// | liquidator could get collateral of (repaidDebt * max(LICR, min(ICR, MCR)) / price) |
||
| 90 |
// |
||
| 91 |
// > MCR & < TCR | only liquidatable in Recovery Mode (TCR < CCR) |
||
| 92 |
// | debt could be fully repaid by liquidator |
||
| 93 |
// | and up to (repaid debt * MCR) worth of collateral |
||
| 94 |
// | transferred to liquidator while the residue of collateral |
||
| 95 |
// | will be available in CollSurplusPool for owner to claim |
||
| 96 |
// | OR debt could be partially repaid by liquidator and |
||
| 97 |
// | liquidator could get collateral of (repaidDebt * max(LICR, min(ICR, MCR)) / price) |
||
| 98 |
// ----------------------------------------------------------------- |
||
| 99 | |||
| 100 |
/// @notice Fully liquidate a single CDP by ID. CDP must meet the criteria for liquidation at the time of execution. |
||
| 101 |
/// @notice callable by anyone, attempts to liquidate the CdpId. Executes successfully if Cdp meets the conditions for liquidation (e.g. in Normal Mode, it liquidates if the Cdp's ICR < the system MCR). |
||
| 102 |
/// @dev forwards msg.data directly to the liquidation library using OZ proxy core delegation function |
||
| 103 |
/// @param _cdpId ID of the CDP to liquidate. |
||
| 104 | |||
| 105 |
function liquidate(bytes32 _cdpId) external override {
|
||
| 106 |
⟳
|
_delegate(liquidationLibrary); |
|
| 107 |
} |
||
| 108 | |||
| 109 |
/// @notice Partially liquidate a single CDP. |
||
| 110 |
/// @dev forwards msg.data directly to the liquidation library using OZ proxy core delegation function |
||
| 111 |
/// @param _cdpId ID of the CDP to partially liquidate. |
||
| 112 |
/// @param _partialAmount Amount to partially liquidate. |
||
| 113 |
/// @param _upperPartialHint Upper hint for reinsertion of the CDP into the linked list. |
||
| 114 |
/// @param _lowerPartialHint Lower hint for reinsertion of the CDP into the linked list. |
||
| 115 |
function partiallyLiquidate( |
||
| 116 |
bytes32 _cdpId, |
||
| 117 |
uint256 _partialAmount, |
||
| 118 |
bytes32 _upperPartialHint, |
||
| 119 |
bytes32 _lowerPartialHint |
||
| 120 |
) external override {
|
||
| 121 |
√
|
⟳
|
_delegate(liquidationLibrary); |
| 122 |
} |
||
| 123 | |||
| 124 |
// --- Batch/Sequence liquidation functions --- |
||
| 125 | |||
| 126 |
/// @notice Attempt to liquidate a custom list of CDPs provided by the caller |
||
| 127 |
/// @notice Callable by anyone, accepts a custom list of Cdps addresses as an argument. Steps through the provided list and attempts to liquidate every Cdp, until it reaches the end or it runs out of gas. A Cdp is liquidated only if it meets the conditions for liquidation. For a batch of 10 Cdps, the gas costs per liquidated Cdp are roughly between 75K-83K, for a batch of 50 Cdps between 54K-69K. |
||
| 128 |
/// @dev forwards msg.data directly to the liquidation library using OZ proxy core delegation function |
||
| 129 |
/// @param _cdpArray Array of CDPs to liquidate. |
||
| 130 |
function batchLiquidateCdps(bytes32[] memory _cdpArray) external override {
|
||
| 131 |
⟳
|
_delegate(liquidationLibrary); |
|
| 132 |
} |
||
| 133 | |||
| 134 |
// --- Redemption functions --- |
||
| 135 | |||
| 136 |
/// @notice // Redeem as much collateral as possible from given Cdp in exchange for EBTC up to specified maximum |
||
| 137 |
/// @param _redeemColFromCdp Struct containing variables for redeeming collateral. |
||
| 138 |
/// @return singleRedemption Struct containing redemption values. |
||
| 139 |
function _redeemCollateralFromCdp( |
||
| 140 |
SingleRedemptionInputs memory _redeemColFromCdp |
||
| 141 |
⟳
|
) internal returns (SingleRedemptionValues memory singleRedemption) {
|
|
| 142 |
// Determine the remaining amount (lot) to be redeemed, |
||
| 143 |
// capped by the entire debt of the Cdp minus the liquidation reserve |
||
| 144 |
⟳
|
singleRedemption.debtToRedeem = EbtcMath._min( |
|
| 145 |
⟳
|
_redeemColFromCdp.maxEBTCamount, |
|
| 146 |
⟳
|
Cdps[_redeemColFromCdp.cdpId].debt /// @audit Redeem everything |
|
| 147 |
); |
||
| 148 | |||
| 149 |
⟳
|
singleRedemption.collSharesDrawn = collateral.getSharesByPooledEth( |
|
| 150 |
⟳
|
(singleRedemption.debtToRedeem * DECIMAL_PRECISION) / _redeemColFromCdp.price |
|
| 151 |
); |
||
| 152 | |||
| 153 |
// Repurposing this struct here to avoid stack too deep. |
||
| 154 |
⟳
|
CdpDebtAndCollShares memory _oldDebtAndColl = CdpDebtAndCollShares( |
|
| 155 |
⟳
|
Cdps[_redeemColFromCdp.cdpId].debt, |
|
| 156 |
⟳
|
Cdps[_redeemColFromCdp.cdpId].coll, |
|
| 157 |
⟳
|
0 |
|
| 158 |
); |
||
| 159 | |||
| 160 |
// Decrease the debt and collateral of the current Cdp according to the EBTC lot and corresponding ETH to send |
||
| 161 |
⟳
|
uint256 newDebt = _oldDebtAndColl.entireDebt - singleRedemption.debtToRedeem; |
|
| 162 |
⟳
|
uint256 newColl = _oldDebtAndColl.entireColl - singleRedemption.collSharesDrawn; |
|
| 163 | |||
| 164 |
⟳
|
if (newDebt == 0) {
|
|
| 165 |
// No debt remains, close CDP |
||
| 166 |
// No debt left in the Cdp, therefore the cdp gets closed |
||
| 167 |
{
|
||
| 168 |
⟳
|
address _borrower = sortedCdps.getOwnerAddress(_redeemColFromCdp.cdpId); |
|
| 169 |
⟳
|
uint256 _liquidatorRewardShares = Cdps[_redeemColFromCdp.cdpId] |
|
| 170 |
.liquidatorRewardShares; |
||
| 171 | |||
| 172 |
⟳
|
singleRedemption.collSurplus = newColl; // Collateral surplus processed on full redemption |
|
| 173 |
⟳
|
singleRedemption.liquidatorRewardShares = _liquidatorRewardShares; |
|
| 174 |
⟳
|
singleRedemption.fullRedemption = true; |
|
| 175 | |||
| 176 |
⟳
|
_closeCdpByRedemption( |
|
| 177 |
⟳
|
_redeemColFromCdp.cdpId, |
|
| 178 |
⟳
|
0, |
|
| 179 |
⟳
|
newColl, |
|
| 180 |
⟳
|
_liquidatorRewardShares, |
|
| 181 |
⟳
|
_borrower |
|
| 182 |
); |
||
| 183 | |||
| 184 |
emit CdpUpdated( |
||
| 185 |
⟳
|
_redeemColFromCdp.cdpId, |
|
| 186 |
⟳
|
_borrower, |
|
| 187 |
⟳
|
_oldDebtAndColl.entireDebt, |
|
| 188 |
⟳
|
_oldDebtAndColl.entireColl, |
|
| 189 |
⟳
|
0, |
|
| 190 |
⟳
|
0, |
|
| 191 |
⟳
|
0, |
|
| 192 |
⟳
|
CdpOperation.redeemCollateral |
|
| 193 |
); |
||
| 194 |
} |
||
| 195 |
} else {
|
||
| 196 |
// Debt remains, reinsert CDP |
||
| 197 |
⟳
|
uint256 newNICR = EbtcMath._computeNominalCR(newColl, newDebt); |
|
| 198 | |||
| 199 |
/* |
||
| 200 |
* If the provided hint is out of date, we bail since trying to reinsert without a good hint will almost |
||
| 201 |
* certainly result in running out of gas. |
||
| 202 |
* |
||
| 203 |
* If the resultant net coll of the partial is less than the minimum, we bail. |
||
| 204 |
*/ |
||
| 205 |
if ( |
||
| 206 |
⟳
|
newNICR != _redeemColFromCdp.partialRedemptionHintNICR || |
|
| 207 |
collateral.getPooledEthByShares(newColl) < MIN_NET_COLL |
||
| 208 |
) {
|
||
| 209 |
⟳
|
singleRedemption.cancelledPartial = true; |
|
| 210 |
⟳
|
return singleRedemption; |
|
| 211 |
} |
||
| 212 | |||
| 213 |
sortedCdps.reInsert( |
||
| 214 |
_redeemColFromCdp.cdpId, |
||
| 215 |
newNICR, |
||
| 216 |
_redeemColFromCdp.upperPartialRedemptionHint, |
||
| 217 |
_redeemColFromCdp.lowerPartialRedemptionHint |
||
| 218 |
); |
||
| 219 | |||
| 220 |
Cdps[_redeemColFromCdp.cdpId].debt = newDebt; |
||
| 221 |
Cdps[_redeemColFromCdp.cdpId].coll = newColl; |
||
| 222 |
_updateStakeAndTotalStakes(_redeemColFromCdp.cdpId); |
||
| 223 | |||
| 224 |
address _borrower = ISortedCdps(sortedCdps).getOwnerAddress(_redeemColFromCdp.cdpId); |
||
| 225 |
emit CdpUpdated( |
||
| 226 |
_redeemColFromCdp.cdpId, |
||
| 227 |
_borrower, |
||
| 228 |
_oldDebtAndColl.entireDebt, |
||
| 229 |
_oldDebtAndColl.entireColl, |
||
| 230 |
newDebt, |
||
| 231 |
newColl, |
||
| 232 |
Cdps[_redeemColFromCdp.cdpId].stake, |
||
| 233 |
CdpOperation.redeemCollateral |
||
| 234 |
); |
||
| 235 |
} |
||
| 236 | |||
| 237 |
⟳
|
return singleRedemption; |
|
| 238 |
} |
||
| 239 | |||
| 240 |
/* |
||
| 241 |
* Called when a full redemption occurs, and closes the cdp. |
||
| 242 |
* The redeemer swaps (debt) EBTC for (debt) |
||
| 243 |
* worth of stETH, so the stETH liquidation reserve is all that remains. |
||
| 244 |
* In order to close the cdp, the stETH liquidation reserve is returned to the CDP owner, |
||
| 245 |
* The debt recorded on the cdp's struct is zero'd elswhere, in _closeCdp. |
||
| 246 |
* Any surplus stETH left in the cdp, is sent to the Coll surplus pool, and can be later claimed by the borrower. |
||
| 247 |
*/ |
||
| 248 |
function _closeCdpByRedemption( |
||
| 249 |
bytes32 _cdpId, // TODO: Remove? |
||
| 250 |
uint256 _EBTC, |
||
| 251 |
uint256 _collSurplus, |
||
| 252 |
uint256 _liquidatorRewardShares, |
||
| 253 |
address _borrower |
||
| 254 |
) internal {
|
||
| 255 |
⟳
|
_closeCdpWithoutRemovingSortedCdps(_cdpId, Status.closedByRedemption); |
|
| 256 | |||
| 257 |
// Update Active Pool EBTC, and send ETH to account |
||
| 258 |
⟳
|
activePool.decreaseSystemDebt(_EBTC); |
|
| 259 | |||
| 260 |
// Register stETH surplus from upcoming transfers of stETH collateral and liquidator reward shares |
||
| 261 |
⟳
|
collSurplusPool.increaseSurplusCollShares(_borrower, _collSurplus + _liquidatorRewardShares); |
|
| 262 | |||
| 263 |
// CEI: send stETH coll and liquidator reward shares from Active Pool to CollSurplus Pool |
||
| 264 |
⟳
|
activePool.transferSystemCollSharesAndLiquidatorReward( |
|
| 265 |
⟳
|
address(collSurplusPool), |
|
| 266 |
⟳
|
_collSurplus, |
|
| 267 |
⟳
|
_liquidatorRewardShares |
|
| 268 |
); |
||
| 269 |
} |
||
| 270 | |||
| 271 |
/// @notice Returns true if the CdpId specified is the lowest-ICR Cdp in the linked list that still has MCR > ICR |
||
| 272 |
/// @dev Returns false if the specified CdpId hint is blank |
||
| 273 |
/// @dev Returns false if the specified CdpId hint doesn't exist in the list |
||
| 274 |
/// @dev Returns false if the ICR of the specified CdpId is < MCR |
||
| 275 |
/// @dev Returns true if the specified CdpId is not blank, exists in the list, has an ICR > MCR, and the next lower Cdp in the list is either blank or has an ICR < MCR. |
||
| 276 |
function _isValidFirstRedemptionHint( |
||
| 277 |
bytes32 _firstRedemptionHint, |
||
| 278 |
uint256 _price |
||
| 279 |
⟳
|
) internal view returns (bool) {
|
|
| 280 |
if ( |
||
| 281 |
⟳
|
_firstRedemptionHint == sortedCdps.nonExistId() || |
|
| 282 |
!sortedCdps.contains(_firstRedemptionHint) || |
||
| 283 |
getSyncedICR(_firstRedemptionHint, _price) < MCR |
||
| 284 |
) {
|
||
| 285 |
⟳
|
return false; |
|
| 286 |
} |
||
| 287 | |||
| 288 |
bytes32 nextCdp = sortedCdps.getNext(_firstRedemptionHint); |
||
| 289 |
return nextCdp == sortedCdps.nonExistId() || getSyncedICR(nextCdp, _price) < MCR; |
||
| 290 |
} |
||
| 291 | |||
| 292 |
/** |
||
| 293 |
redeems `_debt` of eBTC for stETH from the system. Decreases the caller’s eBTC balance, and sends them the corresponding amount of stETH. Executes successfully if the caller has sufficient eBTC to redeem. The number of Cdps redeemed from is capped by `_maxIterations`. The borrower has to provide a `_maxFeePercentage` that he/she is willing to accept in case of a fee slippage, i.e. when another redemption transaction is processed first, driving up the redemption fee. |
||
| 294 |
*/ |
||
| 295 | |||
| 296 |
/* Send _debt EBTC to the system and redeem the corresponding amount of collateral |
||
| 297 |
* from as many Cdps as are needed to fill the redemption |
||
| 298 |
* request. Applies pending rewards to a Cdp before reducing its debt and coll. |
||
| 299 |
* |
||
| 300 |
* Note that if _amount is very large, this function can run out of gas, specially if traversed cdps are small. |
||
| 301 |
* This can be easily avoided by |
||
| 302 |
* splitting the total _amount in appropriate chunks and calling the function multiple times. |
||
| 303 |
* |
||
| 304 |
* Param `_maxIterations` can also be provided, so the loop through Cdps is capped |
||
| 305 |
* (if it’s zero, it will be ignored).This makes it easier to |
||
| 306 |
* avoid OOG for the frontend, as only knowing approximately the average cost of an iteration is enough, |
||
| 307 |
* without needing to know the “topology” |
||
| 308 |
* of the cdp list. It also avoids the need to set the cap in stone in the contract, |
||
| 309 |
* nor doing gas calculations, as both gas price and opcode costs can vary. |
||
| 310 |
* |
||
| 311 |
* All Cdps that are redeemed from -- with the likely exception of the last one -- will end up with no debt left, |
||
| 312 |
* therefore they will be closed. |
||
| 313 |
* If the last Cdp does have some remaining debt, it has a finite ICR, and the reinsertion |
||
| 314 |
* could be anywhere in the list, therefore it requires a hint. |
||
| 315 |
* A frontend should use getRedemptionHints() to calculate what the ICR of this Cdp will be after redemption, |
||
| 316 |
* and pass a hint for its position |
||
| 317 |
* in the sortedCdps list along with the ICR value that the hint was found for. |
||
| 318 |
* |
||
| 319 |
* If another transaction modifies the list between calling getRedemptionHints() |
||
| 320 |
* and passing the hints to redeemCollateral(), it is very likely that the last (partially) |
||
| 321 |
* redeemed Cdp would end up with a different ICR than what the hint is for. In this case the |
||
| 322 |
* redemption will stop after the last completely redeemed Cdp and the sender will keep the |
||
| 323 |
* remaining EBTC amount, which they can attempt to redeem later. |
||
| 324 |
*/ |
||
| 325 |
function redeemCollateral( |
||
| 326 |
uint256 _debt, |
||
| 327 |
bytes32 _firstRedemptionHint, |
||
| 328 |
bytes32 _upperPartialRedemptionHint, |
||
| 329 |
bytes32 _lowerPartialRedemptionHint, |
||
| 330 |
uint256 _partialRedemptionHintNICR, |
||
| 331 |
uint256 _maxIterations, |
||
| 332 |
uint256 _maxFeePercentage |
||
| 333 |
) external override nonReentrantSelfAndBOps {
|
||
| 334 |
⟳
|
RedemptionTotals memory totals; |
|
| 335 | |||
| 336 |
// early check to ensure redemption is not paused |
||
| 337 |
⟳
|
require(redemptionsPaused == false, "CdpManager: Redemptions Paused"); |
|
| 338 | |||
| 339 |
⟳
|
_requireValidMaxFeePercentage(_maxFeePercentage); |
|
| 340 | |||
| 341 |
⟳
|
_syncGlobalAccounting(); // Apply state, we will syncGracePeriod at end of function |
|
| 342 | |||
| 343 |
⟳
|
totals.price = priceFeed.fetchPrice(); |
|
| 344 |
{
|
||
| 345 |
( |
||
| 346 |
⟳
|
uint256 tcrAtStart, |
|
| 347 |
⟳
|
uint256 systemCollSharesAtStart, |
|
| 348 |
⟳
|
uint256 systemDebtAtStart |
|
| 349 |
⟳
|
) = _getTCRWithSystemDebtAndCollShares(totals.price); |
|
| 350 |
⟳
|
totals.tcrAtStart = tcrAtStart; |
|
| 351 |
⟳
|
totals.systemCollSharesAtStart = systemCollSharesAtStart; |
|
| 352 |
⟳
|
totals.systemDebtAtStart = systemDebtAtStart; |
|
| 353 |
} |
||
| 354 | |||
| 355 |
⟳
|
_requireTCRisNotBelowMCR(totals.price, totals.tcrAtStart); |
|
| 356 |
⟳
|
_requireAmountGreaterThanZero(_debt); |
|
| 357 | |||
| 358 |
⟳
|
_requireEbtcBalanceCoversRedemptionAndWithinSupply( |
|
| 359 |
⟳
|
msg.sender, |
|
| 360 |
⟳
|
_debt, |
|
| 361 |
⟳
|
totals.systemDebtAtStart |
|
| 362 |
); |
||
| 363 | |||
| 364 |
⟳
|
totals.remainingDebtToRedeem = _debt; |
|
| 365 |
⟳
|
address currentBorrower; |
|
| 366 |
⟳
|
bytes32 _cId = _firstRedemptionHint; |
|
| 367 | |||
| 368 |
⟳
|
if (_isValidFirstRedemptionHint(_firstRedemptionHint, totals.price)) {
|
|
| 369 |
currentBorrower = sortedCdps.getOwnerAddress(_firstRedemptionHint); |
||
| 370 |
} else {
|
||
| 371 |
⟳
|
_cId = sortedCdps.getLast(); |
|
| 372 |
⟳
|
currentBorrower = sortedCdps.getOwnerAddress(_cId); |
|
| 373 |
// Find the first cdp with ICR >= MCR |
||
| 374 |
⟳
|
while (currentBorrower != address(0) && getSyncedICR(_cId, totals.price) < MCR) {
|
|
| 375 |
⟳
|
_cId = sortedCdps.getPrev(_cId); |
|
| 376 |
⟳
|
currentBorrower = sortedCdps.getOwnerAddress(_cId); |
|
| 377 |
} |
||
| 378 |
} |
||
| 379 | |||
| 380 |
// Loop through the Cdps starting from the one with lowest collateral |
||
| 381 |
// ratio until _amount of EBTC is exchanged for collateral |
||
| 382 |
⟳
|
if (_maxIterations == 0) {
|
|
| 383 |
⟳
|
_maxIterations = type(uint256).max; |
|
| 384 |
} |
||
| 385 | |||
| 386 |
⟳
|
bytes32 _firstRedeemed = _cId; |
|
| 387 |
⟳
|
bytes32 _lastRedeemed = _cId; |
|
| 388 |
⟳
|
uint256 _numCdpsFullyRedeemed; |
|
| 389 | |||
| 390 |
/** |
||
| 391 |
Core Redemption Loop |
||
| 392 |
*/ |
||
| 393 |
while ( |
||
| 394 |
⟳
|
currentBorrower != address(0) && totals.remainingDebtToRedeem > 0 && _maxIterations > 0 |
|
| 395 |
) {
|
||
| 396 |
// Save the address of the Cdp preceding the current one, before potentially modifying the list |
||
| 397 |
{
|
||
| 398 |
⟳
|
_syncAccounting(_cId); /// @audit This happens even if the re-insertion doesn't |
|
| 399 | |||
| 400 |
⟳
|
SingleRedemptionInputs memory _redeemColFromCdp = SingleRedemptionInputs( |
|
| 401 |
⟳
|
_cId, |
|
| 402 |
⟳
|
totals.remainingDebtToRedeem, |
|
| 403 |
⟳
|
totals.price, |
|
| 404 |
⟳
|
_upperPartialRedemptionHint, |
|
| 405 |
⟳
|
_lowerPartialRedemptionHint, |
|
| 406 |
⟳
|
_partialRedemptionHintNICR |
|
| 407 |
); |
||
| 408 |
⟳
|
SingleRedemptionValues memory singleRedemption = _redeemCollateralFromCdp( |
|
| 409 |
⟳
|
_redeemColFromCdp |
|
| 410 |
); |
||
| 411 |
// Partial redemption was cancelled (out-of-date hint, or new net debt < minimum), |
||
| 412 |
// therefore we could not redeem from the last Cdp |
||
| 413 |
⟳
|
if (singleRedemption.cancelledPartial) break; |
|
| 414 | |||
| 415 |
⟳
|
totals.debtToRedeem = totals.debtToRedeem + singleRedemption.debtToRedeem; |
|
| 416 |
⟳
|
totals.collSharesDrawn = totals.collSharesDrawn + singleRedemption.collSharesDrawn; |
|
| 417 |
⟳
|
totals.remainingDebtToRedeem = |
|
| 418 |
⟳
|
totals.remainingDebtToRedeem - |
|
| 419 |
⟳
|
singleRedemption.debtToRedeem; |
|
| 420 |
⟳
|
totals.totalCollSharesSurplus = |
|
| 421 |
⟳
|
totals.totalCollSharesSurplus + |
|
| 422 |
⟳
|
singleRedemption.collSurplus; |
|
| 423 | |||
| 424 |
⟳
|
if (singleRedemption.fullRedemption) {
|
|
| 425 |
⟳
|
_lastRedeemed = _cId; |
|
| 426 |
⟳
|
_numCdpsFullyRedeemed = _numCdpsFullyRedeemed + 1; |
|
| 427 |
} |
||
| 428 | |||
| 429 |
⟳
|
bytes32 _nextId = sortedCdps.getPrev(_cId); |
|
| 430 |
⟳
|
address nextUserToCheck = sortedCdps.getOwnerAddress(_nextId); |
|
| 431 |
⟳
|
currentBorrower = nextUserToCheck; |
|
| 432 |
⟳
|
_cId = _nextId; |
|
| 433 |
} |
||
| 434 |
⟳
|
_maxIterations--; |
|
| 435 |
} |
||
| 436 |
⟳
|
require(totals.collSharesDrawn > 0, "CdpManager: Unable to redeem any amount"); |
|
| 437 | |||
| 438 |
// remove from sortedCdps |
||
| 439 |
⟳
|
if (_numCdpsFullyRedeemed == 1) {
|
|
| 440 |
⟳
|
sortedCdps.remove(_firstRedeemed); |
|
| 441 |
} else if (_numCdpsFullyRedeemed > 1) {
|
||
| 442 |
bytes32[] memory _toRemoveIds = _getCdpIdsToRemove( |
||
| 443 |
_lastRedeemed, |
||
| 444 |
_numCdpsFullyRedeemed, |
||
| 445 |
_firstRedeemed |
||
| 446 |
); |
||
| 447 |
sortedCdps.batchRemove(_toRemoveIds); |
||
| 448 |
} |
||
| 449 | |||
| 450 |
// Decay the baseRate due to time passed, and then increase it according to the size of this redemption. |
||
| 451 |
// Use the saved total EBTC supply value, from before it was reduced by the redemption. |
||
| 452 |
⟳
|
_updateBaseRateFromRedemption( |
|
| 453 |
⟳
|
totals.collSharesDrawn, |
|
| 454 |
⟳
|
totals.price, |
|
| 455 |
⟳
|
totals.systemDebtAtStart |
|
| 456 |
); |
||
| 457 | |||
| 458 |
// Calculate the ETH fee |
||
| 459 |
⟳
|
totals.feeCollShares = _getRedemptionFee(totals.collSharesDrawn); |
|
| 460 | |||
| 461 |
⟳
|
_requireUserAcceptsFee(totals.feeCollShares, totals.collSharesDrawn, _maxFeePercentage); |
|
| 462 | |||
| 463 |
⟳
|
totals.collSharesToRedeemer = totals.collSharesDrawn - totals.feeCollShares; |
|
| 464 | |||
| 465 |
⟳
|
_syncGracePeriodForGivenValues( |
|
| 466 |
⟳
|
totals.systemCollSharesAtStart - totals.collSharesDrawn - totals.totalCollSharesSurplus, |
|
| 467 |
⟳
|
totals.systemDebtAtStart - totals.debtToRedeem, |
|
| 468 |
⟳
|
totals.price |
|
| 469 |
); |
||
| 470 | |||
| 471 |
emit Redemption( |
||
| 472 |
⟳
|
_debt, |
|
| 473 |
⟳
|
totals.debtToRedeem, |
|
| 474 |
⟳
|
totals.collSharesDrawn, |
|
| 475 |
⟳
|
totals.feeCollShares, |
|
| 476 |
⟳
|
msg.sender |
|
| 477 |
); |
||
| 478 | |||
| 479 |
// Burn the total eBTC that is redeemed |
||
| 480 |
⟳
|
ebtcToken.burn(msg.sender, totals.debtToRedeem); |
|
| 481 | |||
| 482 |
// Update Active Pool eBTC debt internal accounting |
||
| 483 |
⟳
|
activePool.decreaseSystemDebt(totals.debtToRedeem); |
|
| 484 | |||
| 485 |
// Allocate the stETH fee to the FeeRecipient |
||
| 486 |
⟳
|
activePool.allocateSystemCollSharesToFeeRecipient(totals.feeCollShares); |
|
| 487 | |||
| 488 |
// CEI: Send the stETH drawn to the redeemer |
||
| 489 |
⟳
|
activePool.transferSystemCollShares(msg.sender, totals.collSharesToRedeemer); |
|
| 490 |
} |
||
| 491 | |||
| 492 |
// --- Helper functions --- |
||
| 493 | |||
| 494 |
function _getCdpIdsToRemove( |
||
| 495 |
bytes32 _start, |
||
| 496 |
uint256 _total, |
||
| 497 |
bytes32 _end |
||
| 498 |
) internal view returns (bytes32[] memory) {
|
||
| 499 |
uint256 _cnt = _total; |
||
| 500 |
bytes32 _id = _start; |
||
| 501 |
bytes32[] memory _toRemoveIds = new bytes32[](_total); |
||
| 502 |
while (_cnt > 0 && _id != bytes32(0)) {
|
||
| 503 |
_toRemoveIds[_total - _cnt] = _id; |
||
| 504 |
_cnt = _cnt - 1; |
||
| 505 |
_id = sortedCdps.getNext(_id); |
||
| 506 |
} |
||
| 507 |
require(_toRemoveIds[0] == _start, "CdpManager: batchRemoveSortedCdpIds check start error"); |
||
| 508 |
require( |
||
| 509 |
_toRemoveIds[_total - 1] == _end, |
||
| 510 |
"CdpManager: batchRemoveSortedCdpIds check end error" |
||
| 511 |
); |
||
| 512 |
return _toRemoveIds; |
||
| 513 |
} |
||
| 514 | |||
| 515 |
function syncAccounting(bytes32 _cdpId) external override {
|
||
| 516 |
// _requireCallerIsBorrowerOperations(); /// @audit Please check this and let us know if opening this creates issues | TODO: See Stermi Partial Liq |
||
| 517 |
√
|
⟳
|
return _syncAccounting(_cdpId); |
| 518 |
} |
||
| 519 | |||
| 520 |
// get totalStakes after split fee taken removed |
||
| 521 |
function getTotalStakeForFeeTaken( |
||
| 522 |
uint256 _feeTaken |
||
| 523 |
) public view override returns (uint256, uint256) {
|
||
| 524 |
uint256 stake = _computeNewStake(_feeTaken); |
||
| 525 |
uint256 _newTotalStakes = totalStakes - stake; |
||
| 526 |
return (_newTotalStakes, stake); |
||
| 527 |
} |
||
| 528 | |||
| 529 |
function updateStakeAndTotalStakes(bytes32 _cdpId) external override returns (uint256) {
|
||
| 530 |
_requireCallerIsBorrowerOperations(); |
||
| 531 |
return _updateStakeAndTotalStakes(_cdpId); |
||
| 532 |
} |
||
| 533 | |||
| 534 |
function closeCdp( |
||
| 535 |
bytes32 _cdpId, |
||
| 536 |
address _borrower, |
||
| 537 |
uint256 _debt, |
||
| 538 |
uint256 _coll |
||
| 539 |
) external override {
|
||
| 540 |
⟳
|
_requireCallerIsBorrowerOperations(); |
|
| 541 |
⟳
|
emit CdpUpdated(_cdpId, _borrower, _debt, _coll, 0, 0, 0, CdpOperation.closeCdp); |
|
| 542 |
⟳
|
return _closeCdp(_cdpId, Status.closedByOwner); |
|
| 543 |
} |
||
| 544 | |||
| 545 |
// Push the owner's address to the Cdp owners list, and record the corresponding array index on the Cdp struct |
||
| 546 |
√
|
function _addCdpIdToArray(bytes32 _cdpId) internal returns (uint128 index) {
|
|
| 547 |
/* Max array size is 2**128 - 1, i.e. ~3e30 cdps. No risk of overflow, since cdps have minimum EBTC |
||
| 548 |
debt of liquidation reserve plus MIN_NET_DEBT. |
||
| 549 |
3e30 EBTC dwarfs the value of all wealth in the world ( which is < 1e15 USD). */ |
||
| 550 | |||
| 551 |
// Push the Cdpowner to the array |
||
| 552 |
√
|
CdpIds.push(_cdpId); |
|
| 553 | |||
| 554 |
// Record the index of the new Cdpowner on their Cdp struct |
||
| 555 |
√
|
index = uint128(CdpIds.length - 1); |
|
| 556 |
√
|
Cdps[_cdpId].arrayIndex = index; |
|
| 557 | |||
| 558 |
return index; |
||
| 559 |
} |
||
| 560 | |||
| 561 |
// --- Recovery Mode and TCR functions --- |
||
| 562 | |||
| 563 |
/** |
||
| 564 |
Returns the systemic entire debt assigned to Cdps, i.e. the systemDebt value of the Active Pool. |
||
| 565 |
*/ |
||
| 566 |
√
|
function getSystemDebt() public view returns (uint256 entireSystemDebt) {
|
|
| 567 |
√
|
return _getSystemDebt(); |
|
| 568 |
} |
||
| 569 | |||
| 570 |
/** |
||
| 571 |
returns the total collateralization ratio (TCR) of the system. The TCR is based on the the entire system debt and collateral (including pending rewards). */ |
||
| 572 |
√
|
⟳
|
function getTCR(uint256 _price) external view override returns (uint256) {
|
| 573 |
√
|
⟳
|
return _getTCR(_price); |
| 574 |
} |
||
| 575 | |||
| 576 |
/** |
||
| 577 |
reveals whether or not the system is in Recovery Mode (i.e. whether the Total Collateralization Ratio (TCR) is below the Critical Collateralization Ratio (CCR)). |
||
| 578 |
*/ |
||
| 579 |
√
|
⟳
|
function checkRecoveryMode(uint256 _price) external view override returns (bool) {
|
| 580 |
√
|
⟳
|
return _checkRecoveryMode(_price); |
| 581 |
} |
||
| 582 | |||
| 583 |
// Check whether or not the system *would be* in Recovery Mode, |
||
| 584 |
// given an ETH:USD price, and the entire system coll and debt. |
||
| 585 |
function _checkPotentialRecoveryMode( |
||
| 586 |
uint256 _systemCollShares, |
||
| 587 |
uint256 _systemDebt, |
||
| 588 |
uint256 _price |
||
| 589 |
) internal view returns (bool) {
|
||
| 590 |
uint256 TCR = _computeTCRWithGivenSystemValues(_systemCollShares, _systemDebt, _price); |
||
| 591 |
return TCR < CCR; |
||
| 592 |
} |
||
| 593 | |||
| 594 |
// --- Redemption fee functions --- |
||
| 595 | |||
| 596 |
/* |
||
| 597 |
* This function has two impacts on the baseRate state variable: |
||
| 598 |
* 1) decays the baseRate based on time passed since last redemption or EBTC borrowing operation. |
||
| 599 |
* then, |
||
| 600 |
* 2) increases the baseRate based on the amount redeemed, as a proportion of total supply |
||
| 601 |
*/ |
||
| 602 |
function _updateBaseRateFromRedemption( |
||
| 603 |
uint256 _ETHDrawn, |
||
| 604 |
uint256 _price, |
||
| 605 |
uint256 _totalEBTCSupply |
||
| 606 |
⟳
|
) internal returns (uint256) {
|
|
| 607 |
⟳
|
uint256 decayedBaseRate = _calcDecayedBaseRate(); |
|
| 608 | |||
| 609 |
/* Convert the drawn ETH back to EBTC at face value rate (1 EBTC:1 USD), in order to get |
||
| 610 |
* the fraction of total supply that was redeemed at face value. */ |
||
| 611 |
⟳
|
uint256 redeemedEBTCFraction = (collateral.getPooledEthByShares(_ETHDrawn) * _price) / |
|
| 612 |
⟳
|
_totalEBTCSupply; |
|
| 613 | |||
| 614 |
⟳
|
uint256 newBaseRate = decayedBaseRate + (redeemedEBTCFraction / beta); |
|
| 615 |
⟳
|
newBaseRate = EbtcMath._min(newBaseRate, DECIMAL_PRECISION); // cap baseRate at a maximum of 100% |
|
| 616 |
⟳
|
require(newBaseRate > 0, "CdpManager: new baseRate is zero!"); // Base rate is always non-zero after redemption |
|
| 617 | |||
| 618 |
// Update the baseRate state variable |
||
| 619 |
⟳
|
baseRate = newBaseRate; |
|
| 620 |
⟳
|
emit BaseRateUpdated(newBaseRate); |
|
| 621 | |||
| 622 |
⟳
|
_updateLastRedemptionTimestamp(); |
|
| 623 | |||
| 624 |
⟳
|
return newBaseRate; |
|
| 625 |
} |
||
| 626 | |||
| 627 |
⟳
|
function getRedemptionRate() public view override returns (uint256) {
|
|
| 628 |
⟳
|
return _calcRedemptionRate(baseRate); |
|
| 629 |
} |
||
| 630 | |||
| 631 |
function getRedemptionRateWithDecay() public view override returns (uint256) {
|
||
| 632 |
return _calcRedemptionRate(_calcDecayedBaseRate()); |
||
| 633 |
} |
||
| 634 | |||
| 635 |
⟳
|
function _calcRedemptionRate(uint256 _baseRate) internal view returns (uint256) {
|
|
| 636 |
return |
||
| 637 |
⟳
|
EbtcMath._min( |
|
| 638 |
⟳
|
redemptionFeeFloor + _baseRate, |
|
| 639 |
DECIMAL_PRECISION // cap at a maximum of 100% |
||
| 640 |
); |
||
| 641 |
} |
||
| 642 | |||
| 643 |
⟳
|
function _getRedemptionFee(uint256 _ETHDrawn) internal view returns (uint256) {
|
|
| 644 |
⟳
|
return _calcRedemptionFee(getRedemptionRate(), _ETHDrawn); |
|
| 645 |
} |
||
| 646 | |||
| 647 |
function getRedemptionFeeWithDecay(uint256 _ETHDrawn) external view override returns (uint256) {
|
||
| 648 |
return _calcRedemptionFee(getRedemptionRateWithDecay(), _ETHDrawn); |
||
| 649 |
} |
||
| 650 | |||
| 651 |
function _calcRedemptionFee( |
||
| 652 |
uint256 _redemptionRate, |
||
| 653 |
uint256 _ETHDrawn |
||
| 654 |
⟳
|
) internal pure returns (uint256) {
|
|
| 655 |
⟳
|
uint256 redemptionFee = (_redemptionRate * _ETHDrawn) / DECIMAL_PRECISION; |
|
| 656 |
⟳
|
require(redemptionFee < _ETHDrawn, "CdpManager: Fee would eat up all returned collateral"); |
|
| 657 |
⟳
|
return redemptionFee; |
|
| 658 |
} |
||
| 659 | |||
| 660 |
// Updates the baseRate state variable based on time elapsed since the last redemption or EBTC borrowing operation. |
||
| 661 |
function decayBaseRateFromBorrowing() external override {
|
||
| 662 |
_requireCallerIsBorrowerOperations(); |
||
| 663 | |||
| 664 |
_decayBaseRate(); |
||
| 665 |
} |
||
| 666 | |||
| 667 |
function _decayBaseRate() internal {
|
||
| 668 |
√
|
uint256 decayedBaseRate = _calcDecayedBaseRate(); |
|
| 669 |
√
|
require(decayedBaseRate <= DECIMAL_PRECISION, "CdpManager: baseRate too large!"); // The baseRate can decay to 0 |
|
| 670 | |||
| 671 |
√
|
baseRate = decayedBaseRate; |
|
| 672 |
√
|
emit BaseRateUpdated(decayedBaseRate); |
|
| 673 | |||
| 674 |
√
|
_updateLastRedemptionTimestamp(); |
|
| 675 |
} |
||
| 676 | |||
| 677 |
// --- Internal fee functions --- |
||
| 678 | |||
| 679 |
// Update the last fee operation time only if time passed >= decay interval. This prevents base rate griefing. |
||
| 680 |
function _updateLastRedemptionTimestamp() internal {
|
||
| 681 |
√
|
⟳
|
uint256 timePassed = block.timestamp > lastRedemptionTimestamp |
| 682 |
√
|
⟳
|
? block.timestamp - lastRedemptionTimestamp |
| 683 |
: 0; |
||
| 684 | |||
| 685 |
√
|
⟳
|
if (timePassed >= SECONDS_IN_ONE_MINUTE) {
|
| 686 |
// Using the effective elapsed time that is consumed so far to update lastRedemptionTimestamp |
||
| 687 |
// instead block.timestamp for consistency with _calcDecayedBaseRate() |
||
| 688 |
√
|
⟳
|
lastRedemptionTimestamp += _minutesPassedSinceLastRedemption() * SECONDS_IN_ONE_MINUTE; |
| 689 |
√
|
⟳
|
emit LastRedemptionTimestampUpdated(block.timestamp); |
| 690 |
} |
||
| 691 |
} |
||
| 692 | |||
| 693 |
√
|
⟳
|
function _calcDecayedBaseRate() internal view returns (uint256) {
|
| 694 |
√
|
⟳
|
uint256 minutesPassed = _minutesPassedSinceLastRedemption(); |
| 695 |
√
|
⟳
|
uint256 decayFactor = EbtcMath._decPow(minuteDecayFactor, minutesPassed); |
| 696 | |||
| 697 |
√
|
⟳
|
return (baseRate * decayFactor) / DECIMAL_PRECISION; |
| 698 |
} |
||
| 699 | |||
| 700 |
√
|
⟳
|
function _minutesPassedSinceLastRedemption() internal view returns (uint256) {
|
| 701 |
return |
||
| 702 |
√
|
⟳
|
block.timestamp > lastRedemptionTimestamp |
| 703 |
√
|
⟳
|
? ((block.timestamp - lastRedemptionTimestamp) / SECONDS_IN_ONE_MINUTE) |
| 704 |
: 0; |
||
| 705 |
} |
||
| 706 | |||
| 707 |
function getDeploymentStartTime() public view returns (uint256) {
|
||
| 708 |
return deploymentStartTime; |
||
| 709 |
} |
||
| 710 | |||
| 711 |
// Check whether or not the system *would be* in Recovery Mode, |
||
| 712 |
// given an ETH:USD price, and the entire system coll and debt. |
||
| 713 |
function checkPotentialRecoveryMode( |
||
| 714 |
uint256 _systemCollShares, |
||
| 715 |
uint256 _systemDebt, |
||
| 716 |
uint256 _price |
||
| 717 |
) external view returns (bool) {
|
||
| 718 |
return _checkPotentialRecoveryMode(_systemCollShares, _systemDebt, _price); |
||
| 719 |
} |
||
| 720 | |||
| 721 |
// --- 'require' wrapper functions --- |
||
| 722 | |||
| 723 |
function _requireEbtcBalanceCoversRedemptionAndWithinSupply( |
||
| 724 |
address _redeemer, |
||
| 725 |
uint256 _amount, |
||
| 726 |
uint256 _totalSupply |
||
| 727 |
) internal view {
|
||
| 728 |
⟳
|
uint256 callerBalance = ebtcToken.balanceOf(_redeemer); |
|
| 729 |
require( |
||
| 730 |
⟳
|
callerBalance >= _amount, |
|
| 731 |
"CdpManager: Requested redemption amount must be <= user's EBTC token balance" |
||
| 732 |
); |
||
| 733 |
require( |
||
| 734 |
⟳
|
callerBalance <= _totalSupply, |
|
| 735 |
"CdpManager: redeemer's EBTC balance exceeds total supply!" |
||
| 736 |
); |
||
| 737 |
} |
||
| 738 | |||
| 739 |
function _requireAmountGreaterThanZero(uint256 _amount) internal pure {
|
||
| 740 |
⟳
|
require(_amount > 0, "CdpManager: Amount must be greater than zero"); |
|
| 741 |
} |
||
| 742 | |||
| 743 |
function _requireTCRisNotBelowMCR(uint256 _price, uint256 _TCR) internal view {
|
||
| 744 |
⟳
|
require(_TCR >= MCR, "CdpManager: Cannot redeem when TCR < MCR"); |
|
| 745 |
} |
||
| 746 | |||
| 747 |
function _requireValidMaxFeePercentage(uint256 _maxFeePercentage) internal view {
|
||
| 748 |
require( |
||
| 749 |
⟳
|
_maxFeePercentage >= redemptionFeeFloor && _maxFeePercentage <= DECIMAL_PRECISION, |
|
| 750 |
"Max fee percentage must be between redemption fee floor and 100%" |
||
| 751 |
); |
||
| 752 |
} |
||
| 753 | |||
| 754 |
// --- Governance Parameters --- |
||
| 755 | |||
| 756 |
function setStakingRewardSplit(uint256 _stakingRewardSplit) external requiresAuth {
|
||
| 757 |
require( |
||
| 758 |
√
|
_stakingRewardSplit <= MAX_REWARD_SPLIT, |
|
| 759 |
"CDPManager: new staking reward split exceeds max" |
||
| 760 |
); |
||
| 761 | |||
| 762 |
√
|
syncGlobalAccountingAndGracePeriod(); |
|
| 763 | |||
| 764 |
√
|
stakingRewardSplit = _stakingRewardSplit; |
|
| 765 |
√
|
emit StakingRewardSplitSet(_stakingRewardSplit); |
|
| 766 |
} |
||
| 767 | |||
| 768 |
function setRedemptionFeeFloor(uint256 _redemptionFeeFloor) external requiresAuth {
|
||
| 769 |
require( |
||
| 770 |
√
|
_redemptionFeeFloor >= MIN_REDEMPTION_FEE_FLOOR, |
|
| 771 |
"CDPManager: new redemption fee floor is lower than minimum" |
||
| 772 |
); |
||
| 773 |
require( |
||
| 774 |
√
|
_redemptionFeeFloor <= DECIMAL_PRECISION, |
|
| 775 |
"CDPManager: new redemption fee floor is higher than maximum" |
||
| 776 |
); |
||
| 777 | |||
| 778 |
√
|
syncGlobalAccountingAndGracePeriod(); |
|
| 779 | |||
| 780 |
√
|
redemptionFeeFloor = _redemptionFeeFloor; |
|
| 781 |
√
|
emit RedemptionFeeFloorSet(_redemptionFeeFloor); |
|
| 782 |
} |
||
| 783 | |||
| 784 |
function setMinuteDecayFactor(uint256 _minuteDecayFactor) external requiresAuth {
|
||
| 785 |
require( |
||
| 786 |
√
|
_minuteDecayFactor >= MIN_MINUTE_DECAY_FACTOR, |
|
| 787 |
"CDPManager: new minute decay factor out of range" |
||
| 788 |
); |
||
| 789 |
require( |
||
| 790 |
√
|
_minuteDecayFactor <= MAX_MINUTE_DECAY_FACTOR, |
|
| 791 |
"CDPManager: new minute decay factor out of range" |
||
| 792 |
); |
||
| 793 | |||
| 794 |
√
|
syncGlobalAccountingAndGracePeriod(); |
|
| 795 | |||
| 796 |
// decay first according to previous factor |
||
| 797 |
√
|
_decayBaseRate(); |
|
| 798 | |||
| 799 |
// set new factor after decaying |
||
| 800 |
√
|
minuteDecayFactor = _minuteDecayFactor; |
|
| 801 |
√
|
emit MinuteDecayFactorSet(_minuteDecayFactor); |
|
| 802 |
} |
||
| 803 | |||
| 804 |
function setBeta(uint256 _beta) external requiresAuth {
|
||
| 805 |
√
|
syncGlobalAccountingAndGracePeriod(); |
|
| 806 | |||
| 807 |
√
|
_decayBaseRate(); |
|
| 808 | |||
| 809 |
√
|
beta = _beta; |
|
| 810 |
√
|
emit BetaSet(_beta); |
|
| 811 |
} |
||
| 812 | |||
| 813 |
function setRedemptionsPaused(bool _paused) external requiresAuth {
|
||
| 814 |
√
|
syncGlobalAccountingAndGracePeriod(); |
|
| 815 |
√
|
_decayBaseRate(); |
|
| 816 | |||
| 817 |
√
|
redemptionsPaused = _paused; |
|
| 818 |
√
|
emit RedemptionsPaused(_paused); |
|
| 819 |
} |
||
| 820 | |||
| 821 |
// --- Cdp property getters --- |
||
| 822 | |||
| 823 |
/// @notice Get status of a CDP. Named values can be found in ICdpManagerData.Status. |
||
| 824 |
√
|
⟳
|
function getCdpStatus(bytes32 _cdpId) external view override returns (uint256) {
|
| 825 |
√
|
⟳
|
return uint256(Cdps[_cdpId].status); |
| 826 |
} |
||
| 827 | |||
| 828 |
/// @notice Get stake value of a CDP. |
||
| 829 |
√
|
function getCdpStake(bytes32 _cdpId) external view override returns (uint256) {
|
|
| 830 |
√
|
return Cdps[_cdpId].stake; |
|
| 831 |
} |
||
| 832 | |||
| 833 |
/// @notice Get stored debt value of a CDP, in eBTC units. Does not include pending changes from redistributions |
||
| 834 |
√
|
⟳
|
function getCdpDebt(bytes32 _cdpId) external view override returns (uint256) {
|
| 835 |
√
|
⟳
|
return Cdps[_cdpId].debt; |
| 836 |
} |
||
| 837 | |||
| 838 |
/// @notice Get stored collateral value of a CDP, in stETH shares. Does not include pending changes from redistributions or unprocessed staking yield. |
||
| 839 |
√
|
⟳
|
function getCdpCollShares(bytes32 _cdpId) external view override returns (uint256) {
|
| 840 |
√
|
⟳
|
return Cdps[_cdpId].coll; |
| 841 |
} |
||
| 842 | |||
| 843 |
/** |
||
| 844 |
@notice Get shares value of the liquidator gas incentive reward stored for a CDP. |
||
| 845 |
@notice This value is processed when a CDP closes. |
||
| 846 |
@dev This value is returned to the borrower when they close their own CDP |
||
| 847 |
@dev This value is given to liquidators upon fully liquidating a CDP |
||
| 848 |
@dev This value is sent to the CollSurplusPool for reclaiming by the borrower when their CDP is redeemed |
||
| 849 |
*/ |
||
| 850 |
√
|
⟳
|
function getCdpLiquidatorRewardShares(bytes32 _cdpId) external view override returns (uint256) {
|
| 851 |
√
|
⟳
|
return Cdps[_cdpId].liquidatorRewardShares; |
| 852 |
} |
||
| 853 | |||
| 854 |
// --- Cdp property setters, called by BorrowerOperations --- |
||
| 855 | |||
| 856 |
/** |
||
| 857 |
@notice Initiailze all state for new CDP |
||
| 858 |
@dev Only callable by BorrowerOperations, critical trust assumption |
||
| 859 |
@dev Requires CDP to be already inserted into linked list correctly |
||
| 860 |
@param _cdpId id of CDP to initialize state for. Inserting the blank CDP into the linked list grants this ID |
||
| 861 |
@param _debt debt units of CDP |
||
| 862 |
@param _coll collateral shares of CDP |
||
| 863 |
@param _liquidatorRewardShares collateral shares for CDP gas stipend |
||
| 864 |
@param _borrower borrower address |
||
| 865 |
*/ |
||
| 866 |
function initializeCdp( |
||
| 867 |
bytes32 _cdpId, |
||
| 868 |
uint256 _debt, |
||
| 869 |
uint256 _coll, |
||
| 870 |
uint256 _liquidatorRewardShares, |
||
| 871 |
address _borrower |
||
| 872 |
) external {
|
||
| 873 |
√
|
_requireCallerIsBorrowerOperations(); |
|
| 874 | |||
| 875 |
√
|
Cdps[_cdpId].debt = _debt; |
|
| 876 |
√
|
Cdps[_cdpId].coll = _coll; |
|
| 877 |
√
|
Cdps[_cdpId].status = Status.active; |
|
| 878 |
√
|
Cdps[_cdpId].liquidatorRewardShares = _liquidatorRewardShares; |
|
| 879 | |||
| 880 |
√
|
_applyAccumulatedFeeSplit(_cdpId); |
|
| 881 |
√
|
_updateRedistributedDebtSnapshot(_cdpId); |
|
| 882 |
√
|
uint256 stake = _updateStakeAndTotalStakes(_cdpId); |
|
| 883 |
√
|
uint256 index = _addCdpIdToArray(_cdpId); |
|
| 884 | |||
| 885 |
// Previous debt and coll are by definition zero upon opening a new CDP |
||
| 886 |
√
|
emit CdpUpdated(_cdpId, _borrower, 0, 0, _debt, _coll, stake, CdpOperation.openCdp); |
|
| 887 |
} |
||
| 888 | |||
| 889 |
/** |
||
| 890 |
@notice Set new CDP debt and collateral values, updating stake accordingly. |
||
| 891 |
@dev Only callable by BorrowerOperations, critical trust assumption |
||
| 892 |
@param _cdpId Id of CDP to update state for |
||
| 893 |
@param _borrower borrower of CDP. Passed along in function to avoid an extra storage read. |
||
| 894 |
@param _coll collateral shares of CDP before update operation. Passed in function to avoid an extra stroage read. |
||
| 895 |
@param _debt debt units of CDP before update operation. Passed in function to avoid an extra stroage read. |
||
| 896 |
@param _newColl collateral shares of CDP after update operation. |
||
| 897 |
@param _newDebt debt units of CDP after update operation. |
||
| 898 |
*/ |
||
| 899 |
function updateCdp( |
||
| 900 |
bytes32 _cdpId, |
||
| 901 |
address _borrower, |
||
| 902 |
uint256 _coll, |
||
| 903 |
uint256 _debt, |
||
| 904 |
uint256 _newColl, |
||
| 905 |
uint256 _newDebt |
||
| 906 |
) external {
|
||
| 907 |
√
|
_requireCallerIsBorrowerOperations(); |
|
| 908 | |||
| 909 |
√
|
_setCdpCollShares(_cdpId, _newColl); |
|
| 910 |
√
|
_setCdpDebt(_cdpId, _newDebt); |
|
| 911 | |||
| 912 |
√
|
uint256 stake = _updateStakeAndTotalStakes(_cdpId); |
|
| 913 | |||
| 914 |
emit CdpUpdated( |
||
| 915 |
√
|
_cdpId, |
|
| 916 |
√
|
_borrower, |
|
| 917 |
√
|
_debt, |
|
| 918 |
√
|
_coll, |
|
| 919 |
√
|
_newDebt, |
|
| 920 |
√
|
_newColl, |
|
| 921 |
√
|
stake, |
|
| 922 |
√
|
CdpOperation.adjustCdp |
|
| 923 |
); |
||
| 924 |
} |
||
| 925 | |||
| 926 |
/** |
||
| 927 |
* @notice Set the collateral of a CDP |
||
| 928 |
* @param _cdpId The ID of the CDP |
||
| 929 |
* @param _newColl New collateral value, in stETH shares |
||
| 930 |
*/ |
||
| 931 |
function _setCdpCollShares(bytes32 _cdpId, uint256 _newColl) internal {
|
||
| 932 |
√
|
Cdps[_cdpId].coll = _newColl; |
|
| 933 |
} |
||
| 934 | |||
| 935 |
/** |
||
| 936 |
* @notice Set the debt of a CDP |
||
| 937 |
* @param _cdpId The ID of the CDP |
||
| 938 |
* @param _newDebt New debt units value |
||
| 939 |
*/ |
||
| 940 |
function _setCdpDebt(bytes32 _cdpId, uint256 _newDebt) internal {
|
||
| 941 |
√
|
Cdps[_cdpId].debt = _newDebt; |
|
| 942 |
} |
||
| 943 |
} |
||
| 944 |
| Lines covered: | 273 / 324 (84.3%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Interfaces/ICdpManager.sol"; |
||
| 6 |
import "./Interfaces/ICollSurplusPool.sol"; |
||
| 7 |
import "./Interfaces/IEBTCToken.sol"; |
||
| 8 |
import "./Interfaces/ISortedCdps.sol"; |
||
| 9 |
import "./Dependencies/EbtcBase.sol"; |
||
| 10 |
import "./Dependencies/ReentrancyGuard.sol"; |
||
| 11 |
import "./Dependencies/ICollateralTokenOracle.sol"; |
||
| 12 |
import "./Dependencies/AuthNoOwner.sol"; |
||
| 13 | |||
| 14 |
/** |
||
| 15 |
@notice CDP Manager storage and shared functions |
||
| 16 |
@dev CDP Manager was split to get around contract size limitations, liquidation related functions are delegated to LiquidationLibrary contract code. |
||
| 17 |
@dev Both must maintain the same storage layout, so shared storage components where placed here |
||
| 18 |
@dev Shared functions were also added here to de-dup code |
||
| 19 |
*/ |
||
| 20 |
contract CdpManagerStorage is EbtcBase, ReentrancyGuard, ICdpManagerData, AuthNoOwner {
|
||
| 21 |
// TODO: IMPROVE |
||
| 22 |
// NOTE: No packing cause it's the last var, no need for u64 |
||
| 23 |
√
|
⟳
|
uint128 public constant UNSET_TIMESTAMP = type(uint128).max; |
| 24 |
√
|
uint128 public constant MINIMUM_GRACE_PERIOD = 15 minutes; |
|
| 25 | |||
| 26 |
// TODO: IMPROVE THIS!!! |
||
| 27 |
√
|
⟳
|
uint128 public lastGracePeriodStartTimestamp = UNSET_TIMESTAMP; // use max to signify |
| 28 |
√
|
⟳
|
uint128 public recoveryModeGracePeriodDuration = MINIMUM_GRACE_PERIOD; |
| 29 | |||
| 30 |
// TODO: Pitfal is fee split // NOTE: Solved by calling `syncGracePeriod` on external operations from BO |
||
| 31 | |||
| 32 |
/// @notice Start the recovery mode grace period, if the system is in RM and the grace period timestamp has not already been set |
||
| 33 |
/// @dev Trusted function to allow BorrowerOperations actions to set RM Grace Period |
||
| 34 |
/// @dev Assumes BorrowerOperations has correctly calculated and passed in the new system TCR |
||
| 35 |
/// @dev To maintain CEI compliance we use this trusted function |
||
| 36 |
function notifyStartGracePeriod(uint256 tcr) external {
|
||
| 37 |
_requireCallerIsBorrowerOperations(); |
||
| 38 |
_startGracePeriod(tcr); |
||
| 39 |
} |
||
| 40 | |||
| 41 |
/// @notice End the recovery mode grace period, if the system is no longer in RM |
||
| 42 |
/// @dev Trusted function to allow BorrowerOperations actions to set RM Grace Period |
||
| 43 |
/// @dev Assumes BorrowerOperations has correctly calculated and passed in the new system TCR |
||
| 44 |
/// @dev To maintain CEI compliance we use this trusted function |
||
| 45 |
function notifyEndGracePeriod(uint256 tcr) external {
|
||
| 46 |
√
|
⟳
|
_requireCallerIsBorrowerOperations(); |
| 47 |
√
|
⟳
|
_endGracePeriod(tcr); |
| 48 |
} |
||
| 49 | |||
| 50 |
/// @dev Internal notify called by Redemptions and Liquidations |
||
| 51 |
/// @dev Specified TCR is emitted for notification pruposes regardless of whether the Grace Period timestamp is set |
||
| 52 |
function _startGracePeriod(uint256 _tcr) internal {
|
||
| 53 |
√
|
⟳
|
emit TCRNotified(_tcr); |
| 54 | |||
| 55 |
√
|
⟳
|
if (lastGracePeriodStartTimestamp == UNSET_TIMESTAMP) {
|
| 56 |
√
|
⟳
|
lastGracePeriodStartTimestamp = uint128(block.timestamp); |
| 57 | |||
| 58 |
√
|
⟳
|
emit GracePeriodStart(); |
| 59 |
} |
||
| 60 |
} |
||
| 61 | |||
| 62 |
/// @notice Clear RM Grace Period timestamp if it has been set |
||
| 63 |
/// @notice No input validation, calling function must confirm that the system is not in recovery mode to be valid |
||
| 64 |
/// @dev Specified TCR is emitted for notification pruposes regardless of whether the Grace Period timestamp is set |
||
| 65 |
/// @dev Internal notify called by Redemptions and Liquidations |
||
| 66 |
function _endGracePeriod(uint256 _tcr) internal {
|
||
| 67 |
√
|
⟳
|
emit TCRNotified(_tcr); |
| 68 | |||
| 69 |
√
|
⟳
|
if (lastGracePeriodStartTimestamp != UNSET_TIMESTAMP) {
|
| 70 |
⟳
|
lastGracePeriodStartTimestamp = UNSET_TIMESTAMP; |
|
| 71 | |||
| 72 |
⟳
|
emit GracePeriodEnd(); |
|
| 73 |
} |
||
| 74 |
} |
||
| 75 | |||
| 76 |
function _syncGracePeriod() internal {
|
||
| 77 |
√
|
⟳
|
uint256 price = priceFeed.fetchPrice(); |
| 78 |
√
|
⟳
|
uint256 tcr = _getTCR(price); |
| 79 |
√
|
⟳
|
bool isRecoveryMode = _checkRecoveryModeForTCR(tcr); |
| 80 | |||
| 81 |
√
|
⟳
|
if (isRecoveryMode) {
|
| 82 |
⟳
|
_startGracePeriod(tcr); |
|
| 83 |
} else {
|
||
| 84 |
√
|
⟳
|
_endGracePeriod(tcr); |
| 85 |
} |
||
| 86 |
} |
||
| 87 | |||
| 88 |
/// @dev Set RM grace period based on specified system collShares, system debt, and price |
||
| 89 |
/// @dev Variant for internal use in redemptions and liquidations |
||
| 90 |
function _syncGracePeriodForGivenValues( |
||
| 91 |
uint256 systemCollShares, |
||
| 92 |
uint256 systemDebt, |
||
| 93 |
uint256 price |
||
| 94 |
) internal {
|
||
| 95 |
// Compute TCR with specified values |
||
| 96 |
√
|
⟳
|
uint256 newTCR = EbtcMath._computeCR( |
| 97 |
√
|
⟳
|
collateral.getPooledEthByShares(systemCollShares), |
| 98 |
√
|
⟳
|
systemDebt, |
| 99 |
√
|
⟳
|
price |
| 100 |
); |
||
| 101 | |||
| 102 |
√
|
⟳
|
if (newTCR < CCR) {
|
| 103 |
// Notify system is in RM |
||
| 104 |
√
|
⟳
|
_startGracePeriod(newTCR); |
| 105 |
} else {
|
||
| 106 |
// Notify system is outside RM |
||
| 107 |
_endGracePeriod(newTCR); |
||
| 108 |
} |
||
| 109 |
} |
||
| 110 | |||
| 111 |
/// @notice Set grace period duratin |
||
| 112 |
/// @notice Permissioned governance function, must set grace period duration above hardcoded minimum |
||
| 113 |
/// @param _gracePeriod new grace period duration, in seconds |
||
| 114 |
function setGracePeriod(uint128 _gracePeriod) external requiresAuth {
|
||
| 115 |
require( |
||
| 116 |
√
|
_gracePeriod >= MINIMUM_GRACE_PERIOD, |
|
| 117 |
"CdpManager: Grace period below minimum duration" |
||
| 118 |
); |
||
| 119 | |||
| 120 |
√
|
syncGlobalAccountingAndGracePeriod(); |
|
| 121 |
√
|
recoveryModeGracePeriodDuration = _gracePeriod; |
|
| 122 |
√
|
emit GracePeriodDurationSet(_gracePeriod); |
|
| 123 |
} |
||
| 124 | |||
| 125 |
string public constant NAME = "CdpManager"; |
||
| 126 | |||
| 127 |
// --- Connected contract declarations --- |
||
| 128 | |||
| 129 |
address public immutable borrowerOperationsAddress; |
||
| 130 | |||
| 131 |
ICollSurplusPool immutable collSurplusPool; |
||
| 132 | |||
| 133 |
IEBTCToken public immutable override ebtcToken; |
||
| 134 | |||
| 135 |
address public immutable liquidationLibrary; |
||
| 136 | |||
| 137 |
// A doubly linked list of Cdps, sorted by their sorted by their collateral ratios |
||
| 138 |
√
|
ISortedCdps public immutable sortedCdps; |
|
| 139 | |||
| 140 |
// --- Data structures --- |
||
| 141 | |||
| 142 |
√
|
⟳
|
uint256 public constant SECONDS_IN_ONE_MINUTE = 60; |
| 143 | |||
| 144 |
√
|
uint256 public constant MIN_REDEMPTION_FEE_FLOOR = (DECIMAL_PRECISION * 5) / 1000; // 0.5% |
|
| 145 |
√
|
⟳
|
uint256 public redemptionFeeFloor = MIN_REDEMPTION_FEE_FLOOR; |
| 146 |
bool public redemptionsPaused; |
||
| 147 |
/* |
||
| 148 |
* Half-life of 12h. 12h = 720 min |
||
| 149 |
* (1/2) = d^720 => d = (1/2)^(1/720) |
||
| 150 |
*/ |
||
| 151 |
√
|
uint256 public minuteDecayFactor = 999037758833783000; |
|
| 152 |
√
|
uint256 public constant MIN_MINUTE_DECAY_FACTOR = 1; // Non-zero |
|
| 153 |
√
|
uint256 public constant MAX_MINUTE_DECAY_FACTOR = 999999999999999999; // Corresponds to a very fast decay rate, but not too extreme |
|
| 154 | |||
| 155 |
uint256 internal immutable deploymentStartTime; |
||
| 156 | |||
| 157 |
/* |
||
| 158 |
* BETA: 18 digit decimal. Parameter by which to divide the redeemed fraction, |
||
| 159 |
* in order to calc the new base rate from a redemption. |
||
| 160 |
* Corresponds to (1 / ALPHA) in the white paper. |
||
| 161 |
*/ |
||
| 162 |
√
|
uint256 public beta = 2; |
|
| 163 | |||
| 164 |
uint256 public baseRate; |
||
| 165 | |||
| 166 |
uint256 public stakingRewardSplit; |
||
| 167 | |||
| 168 |
// The timestamp of the latest fee operation (redemption or new EBTC issuance) |
||
| 169 |
uint256 public lastRedemptionTimestamp; |
||
| 170 | |||
| 171 |
mapping(bytes32 => Cdp) public Cdps; |
||
| 172 | |||
| 173 |
√
|
uint256 public override totalStakes; |
|
| 174 | |||
| 175 |
// Snapshot of the value of totalStakes, taken immediately after the latest liquidation and split fee claim |
||
| 176 |
uint256 public totalStakesSnapshot; |
||
| 177 | |||
| 178 |
// Snapshot of the total collateral across the ActivePool, immediately after the latest liquidation and split fee claim |
||
| 179 |
uint256 public totalCollateralSnapshot; |
||
| 180 | |||
| 181 |
/* |
||
| 182 |
* systemDebtRedistributionIndex track the sums of accumulated liquidation rewards per unit staked. |
||
| 183 |
* During its lifetime, each stake earns: |
||
| 184 |
* |
||
| 185 |
* A systemDebt increase of ( stake * [systemDebtRedistributionIndex - systemDebtRedistributionIndex(0)] ) |
||
| 186 |
* |
||
| 187 |
* Where systemDebtRedistributionIndex(0) are snapshots of systemDebtRedistributionIndex |
||
| 188 |
* for the active Cdp taken at the instant the stake was made |
||
| 189 |
*/ |
||
| 190 |
√
|
⟳
|
uint256 public systemDebtRedistributionIndex; |
| 191 | |||
| 192 |
/* Global Index for (Full Price Per Share) of underlying collateral token */ |
||
| 193 |
√
|
⟳
|
uint256 public override stEthIndex; |
| 194 |
/* Global Fee accumulator (never decreasing) per stake unit in CDPManager, similar to systemDebtRedistributionIndex */ |
||
| 195 |
√
|
uint256 public override systemStEthFeePerUnitIndex; |
|
| 196 |
/* Global Fee accumulator calculation error due to integer division, similar to redistribution calculation */ |
||
| 197 |
uint256 public override systemStEthFeePerUnitIndexError; |
||
| 198 |
/* Individual CDP Fee accumulator tracker, used to calculate fee split distribution */ |
||
| 199 |
√
|
mapping(bytes32 => uint256) public cdpStEthFeePerUnitIndex; |
|
| 200 |
/* Update timestamp for global index */ |
||
| 201 |
uint256 lastIndexTimestamp; |
||
| 202 |
// Map active cdps to their RewardSnapshot (eBTC debt redistributed) |
||
| 203 |
mapping(bytes32 => uint256) public cdpDebtRedistributionIndex; |
||
| 204 | |||
| 205 |
// Array of all active cdp Ids - used to to compute an approximate hint off-chain, for the sorted list insertion |
||
| 206 |
√
|
bytes32[] public CdpIds; |
|
| 207 | |||
| 208 |
// Error trackers for the cdp redistribution calculation |
||
| 209 |
uint256 public lastEBTCDebtErrorRedistribution; |
||
| 210 | |||
| 211 |
constructor( |
||
| 212 |
address _liquidationLibraryAddress, |
||
| 213 |
address _authorityAddress, |
||
| 214 |
address _borrowerOperationsAddress, |
||
| 215 |
address _collSurplusPool, |
||
| 216 |
address _ebtcToken, |
||
| 217 |
address _sortedCdps, |
||
| 218 |
address _activePool, |
||
| 219 |
address _priceFeed, |
||
| 220 |
address _collateral |
||
| 221 |
√
|
) EbtcBase(_activePool, _priceFeed, _collateral) {
|
|
| 222 |
// TODO: Move to setAddresses or _tickInterest? |
||
| 223 |
√
|
deploymentStartTime = block.timestamp; |
|
| 224 |
√
|
liquidationLibrary = _liquidationLibraryAddress; |
|
| 225 | |||
| 226 |
√
|
_initializeAuthority(_authorityAddress); |
|
| 227 | |||
| 228 |
√
|
borrowerOperationsAddress = _borrowerOperationsAddress; |
|
| 229 |
√
|
collSurplusPool = ICollSurplusPool(_collSurplusPool); |
|
| 230 |
√
|
ebtcToken = IEBTCToken(_ebtcToken); |
|
| 231 |
√
|
sortedCdps = ISortedCdps(_sortedCdps); |
|
| 232 |
} |
||
| 233 | |||
| 234 |
/** |
||
| 235 |
@notice BorrowerOperations and CdpManager share reentrancy status by confirming the other's locked flag before beginning operation |
||
| 236 |
@dev This is an alternative to the more heavyweight solution of both being able to set the reentrancy flag on a 3rd contract. |
||
| 237 |
*/ |
||
| 238 |
modifier nonReentrantSelfAndBOps() {
|
||
| 239 |
√
|
⟳
|
require(locked == OPEN, "CdpManager: Reentrancy in nonReentrant call"); |
| 240 |
require( |
||
| 241 |
√
|
⟳
|
ReentrancyGuard(borrowerOperationsAddress).locked() == OPEN, |
| 242 |
"BorrowerOperations: Reentrancy in nonReentrant call" |
||
| 243 |
); |
||
| 244 | |||
| 245 |
√
|
⟳
|
locked = LOCKED; |
| 246 | |||
| 247 |
_; |
||
| 248 | |||
| 249 |
√
|
⟳
|
locked = OPEN; |
| 250 |
} |
||
| 251 | |||
| 252 |
function _closeCdp(bytes32 _cdpId, Status closedStatus) internal {
|
||
| 253 |
⟳
|
_closeCdpWithoutRemovingSortedCdps(_cdpId, closedStatus); |
|
| 254 |
⟳
|
sortedCdps.remove(_cdpId); |
|
| 255 |
} |
||
| 256 | |||
| 257 |
function _closeCdpWithoutRemovingSortedCdps(bytes32 _cdpId, Status closedStatus) internal {
|
||
| 258 |
require( |
||
| 259 |
⟳
|
closedStatus != Status.nonExistent && closedStatus != Status.active, |
|
| 260 |
"CdpManagerStorage: close non-exist or non-active CDP!" |
||
| 261 |
); |
||
| 262 | |||
| 263 |
⟳
|
uint256 cdpIdsArrayLength = CdpIds.length; |
|
| 264 |
⟳
|
_requireMoreThanOneCdpInSystem(cdpIdsArrayLength); |
|
| 265 | |||
| 266 |
⟳
|
_removeStake(_cdpId); |
|
| 267 | |||
| 268 |
⟳
|
Cdps[_cdpId].status = closedStatus; |
|
| 269 |
⟳
|
Cdps[_cdpId].coll = 0; |
|
| 270 |
⟳
|
Cdps[_cdpId].debt = 0; |
|
| 271 |
⟳
|
Cdps[_cdpId].liquidatorRewardShares = 0; |
|
| 272 | |||
| 273 |
⟳
|
cdpDebtRedistributionIndex[_cdpId] = 0; |
|
| 274 |
⟳
|
cdpStEthFeePerUnitIndex[_cdpId] = 0; |
|
| 275 | |||
| 276 |
⟳
|
_removeCdp(_cdpId, cdpIdsArrayLength); |
|
| 277 |
} |
||
| 278 | |||
| 279 |
/* |
||
| 280 |
* Updates snapshots of system total stakes and total collateral, |
||
| 281 |
* excluding a given collateral remainder from the calculation. |
||
| 282 |
* Used in a liquidation sequence. |
||
| 283 |
* |
||
| 284 |
* The calculation excludes a portion of collateral that is in the ActivePool: |
||
| 285 |
* |
||
| 286 |
* the total ETH gas compensation from the liquidation sequence |
||
| 287 |
* |
||
| 288 |
* The ETH as compensation must be excluded as it is always sent out at the very end of the liquidation sequence. |
||
| 289 |
*/ |
||
| 290 |
function _updateSystemSnapshotsExcludeCollRemainder(uint256 _collRemainder) internal {
|
||
| 291 |
√
|
⟳
|
uint256 _totalStakesSnapshot = totalStakes; |
| 292 |
√
|
⟳
|
totalStakesSnapshot = _totalStakesSnapshot; |
| 293 | |||
| 294 |
√
|
⟳
|
uint256 _totalCollateralSnapshot = activePool.getSystemCollShares() - _collRemainder; |
| 295 |
√
|
⟳
|
totalCollateralSnapshot = _totalCollateralSnapshot; |
| 296 | |||
| 297 |
√
|
⟳
|
emit SystemSnapshotsUpdated(_totalStakesSnapshot, _totalCollateralSnapshot); |
| 298 |
} |
||
| 299 | |||
| 300 |
/** |
||
| 301 |
get the pending Cdp debt "reward" (i.e. the amount of extra debt assigned to the Cdp) from liquidation redistribution events, earned by their stake |
||
| 302 |
*/ |
||
| 303 |
function _getPendingRedistributedDebt( |
||
| 304 |
bytes32 _cdpId |
||
| 305 |
√
|
⟳
|
) internal view returns (uint256 pendingEBTCDebtReward) {
|
| 306 |
√
|
⟳
|
Cdp storage cdp = Cdps[_cdpId]; |
| 307 | |||
| 308 |
√
|
⟳
|
if (cdp.status != Status.active) {
|
| 309 |
√
|
⟳
|
return 0; |
| 310 |
} |
||
| 311 | |||
| 312 |
√
|
⟳
|
uint256 rewardPerUnitStaked = systemDebtRedistributionIndex - |
| 313 |
√
|
⟳
|
cdpDebtRedistributionIndex[_cdpId]; |
| 314 | |||
| 315 |
√
|
⟳
|
if (rewardPerUnitStaked > 0) {
|
| 316 |
pendingEBTCDebtReward = (cdp.stake * rewardPerUnitStaked) / DECIMAL_PRECISION; |
||
| 317 |
} else {
|
||
| 318 |
√
|
⟳
|
return 0; |
| 319 |
} |
||
| 320 |
} |
||
| 321 | |||
| 322 |
function _hasRedistributedDebt(bytes32 _cdpId) internal view returns (bool) {
|
||
| 323 |
/* |
||
| 324 |
* A Cdp has pending rewards if its snapshot is less than the current rewards per-unit-staked sum: |
||
| 325 |
* this indicates that rewards have occured since the snapshot was made, and the user therefore has |
||
| 326 |
* pending rewards |
||
| 327 |
*/ |
||
| 328 |
if (Cdps[_cdpId].status != Status.active) {
|
||
| 329 |
return false; |
||
| 330 |
} |
||
| 331 | |||
| 332 |
// Returns true if there have been any redemptions |
||
| 333 |
return (cdpDebtRedistributionIndex[_cdpId] < systemDebtRedistributionIndex); |
||
| 334 |
} |
||
| 335 | |||
| 336 |
function _updateRedistributedDebtSnapshot(bytes32 _cdpId) internal {
|
||
| 337 |
√
|
uint256 _L_EBTCDebt = systemDebtRedistributionIndex; |
|
| 338 | |||
| 339 |
√
|
cdpDebtRedistributionIndex[_cdpId] = _L_EBTCDebt; |
|
| 340 |
√
|
emit CdpDebtRedistributionIndexUpdated(_cdpId, _L_EBTCDebt); |
|
| 341 |
} |
||
| 342 | |||
| 343 |
// Add the borrowers's coll and debt rewards earned from redistributions, to their Cdp |
||
| 344 |
function _syncAccounting(bytes32 _cdpId) internal {
|
||
| 345 |
√
|
⟳
|
(, uint _newDebt, , uint _pendingDebt) = _applyAccumulatedFeeSplit(_cdpId); |
| 346 | |||
| 347 |
// Update pending debts |
||
| 348 |
√
|
⟳
|
if (_pendingDebt > 0) {
|
| 349 |
Cdp storage _cdp = Cdps[_cdpId]; |
||
| 350 |
uint256 prevDebt = _cdp.debt; |
||
| 351 |
uint256 prevColl = _cdp.coll; |
||
| 352 | |||
| 353 |
// Apply pending rewards to cdp's state |
||
| 354 |
_cdp.debt = _newDebt; |
||
| 355 | |||
| 356 |
_updateRedistributedDebtSnapshot(_cdpId); |
||
| 357 | |||
| 358 |
emit CdpUpdated( |
||
| 359 |
_cdpId, |
||
| 360 |
ISortedCdps(sortedCdps).getOwnerAddress(_cdpId), |
||
| 361 |
prevDebt, |
||
| 362 |
prevColl, |
||
| 363 |
_newDebt, |
||
| 364 |
prevColl, |
||
| 365 |
Cdps[_cdpId].stake, |
||
| 366 |
CdpOperation.syncAccounting |
||
| 367 |
); |
||
| 368 |
} |
||
| 369 |
} |
||
| 370 | |||
| 371 |
// Remove borrower's stake from the totalStakes sum, and set their stake to 0 |
||
| 372 |
function _removeStake(bytes32 _cdpId) internal {
|
||
| 373 |
⟳
|
uint256 _newTotalStakes = totalStakes - Cdps[_cdpId].stake; |
|
| 374 |
⟳
|
totalStakes = _newTotalStakes; |
|
| 375 |
⟳
|
Cdps[_cdpId].stake = 0; |
|
| 376 |
⟳
|
emit TotalStakesUpdated(_newTotalStakes); |
|
| 377 |
} |
||
| 378 | |||
| 379 |
// Update borrower's stake based on their latest collateral value |
||
| 380 |
// and update totalStakes accordingly as well |
||
| 381 |
√
|
function _updateStakeAndTotalStakes(bytes32 _cdpId) internal returns (uint256) {
|
|
| 382 |
√
|
(uint256 newStake, uint256 oldStake) = _updateStakeForCdp(_cdpId); |
|
| 383 | |||
| 384 |
√
|
uint256 _newTotalStakes = totalStakes + newStake - oldStake; |
|
| 385 |
√
|
totalStakes = _newTotalStakes; |
|
| 386 | |||
| 387 |
√
|
emit TotalStakesUpdated(_newTotalStakes); |
|
| 388 | |||
| 389 |
√
|
return newStake; |
|
| 390 |
} |
||
| 391 | |||
| 392 |
// Update borrower's stake based on their latest collateral value |
||
| 393 |
√
|
function _updateStakeForCdp(bytes32 _cdpId) internal returns (uint256, uint256) {
|
|
| 394 |
√
|
Cdp storage _cdp = Cdps[_cdpId]; |
|
| 395 |
√
|
uint256 newStake = _computeNewStake(_cdp.coll); |
|
| 396 |
√
|
uint256 oldStake = _cdp.stake; |
|
| 397 |
√
|
_cdp.stake = newStake; |
|
| 398 | |||
| 399 |
√
|
return (newStake, oldStake); |
|
| 400 |
} |
||
| 401 | |||
| 402 |
// Calculate a new stake based on the snapshots of the totalStakes and totalCollateral taken at the last liquidation |
||
| 403 |
√
|
function _computeNewStake(uint256 _coll) internal view returns (uint256) {
|
|
| 404 |
√
|
uint256 stake; |
|
| 405 |
√
|
if (totalCollateralSnapshot == 0) {
|
|
| 406 |
√
|
stake = _coll; |
|
| 407 |
} else {
|
||
| 408 |
/* |
||
| 409 |
* The following check holds true because: |
||
| 410 |
* - The system always contains >= 1 cdp |
||
| 411 |
* - When we close or liquidate a cdp, we redistribute the pending rewards, |
||
| 412 |
* so if all cdps were closed/liquidated, |
||
| 413 |
* rewards would’ve been emptied and totalCollateralSnapshot would be zero too. |
||
| 414 |
*/ |
||
| 415 |
√
|
require(totalStakesSnapshot > 0, "CdpManagerStorage: zero totalStakesSnapshot!"); |
|
| 416 |
√
|
stake = (_coll * totalStakesSnapshot) / totalCollateralSnapshot; |
|
| 417 |
} |
||
| 418 |
√
|
return stake; |
|
| 419 |
} |
||
| 420 | |||
| 421 |
/* |
||
| 422 |
* Remove a Cdp owner from the CdpOwners array, not preserving array order. Removing owner 'B' does the following: |
||
| 423 |
* [A B C D E] => [A E C D], and updates E's Cdp struct to point to its new array index. |
||
| 424 |
*/ |
||
| 425 |
function _removeCdp(bytes32 _cdpId, uint256 cdpIdsArrayLength) internal {
|
||
| 426 |
⟳
|
Status cdpStatus = Cdps[_cdpId].status; |
|
| 427 |
// It’s set in caller function `_closeCdp` |
||
| 428 |
require( |
||
| 429 |
⟳
|
cdpStatus != Status.nonExistent && cdpStatus != Status.active, |
|
| 430 |
"CdpManagerStorage: remove non-exist or non-active CDP!" |
||
| 431 |
); |
||
| 432 | |||
| 433 |
⟳
|
uint128 index = Cdps[_cdpId].arrayIndex; |
|
| 434 |
⟳
|
uint256 length = cdpIdsArrayLength; |
|
| 435 |
⟳
|
uint256 idxLast = length - 1; |
|
| 436 | |||
| 437 |
⟳
|
require(index <= idxLast, "CdpManagerStorage: CDP indexing overflow!"); |
|
| 438 | |||
| 439 |
⟳
|
bytes32 idToMove = CdpIds[idxLast]; |
|
| 440 | |||
| 441 |
⟳
|
CdpIds[index] = idToMove; |
|
| 442 |
⟳
|
Cdps[idToMove].arrayIndex = index; |
|
| 443 |
⟳
|
emit CdpArrayIndexUpdated(idToMove, index); |
|
| 444 | |||
| 445 |
⟳
|
CdpIds.pop(); |
|
| 446 |
} |
||
| 447 | |||
| 448 |
// --- Recovery Mode and TCR functions --- |
||
| 449 | |||
| 450 |
// Calculate TCR given an price, and the entire system coll and debt. |
||
| 451 |
function _computeTCRWithGivenSystemValues( |
||
| 452 |
uint256 _systemCollShares, |
||
| 453 |
uint256 _systemDebt, |
||
| 454 |
uint256 _price |
||
| 455 |
) internal view returns (uint256) {
|
||
| 456 |
uint256 _totalColl = collateral.getPooledEthByShares(_systemCollShares); |
||
| 457 |
return EbtcMath._computeCR(_totalColl, _systemDebt, _price); |
||
| 458 |
} |
||
| 459 | |||
| 460 |
// --- Staking-Reward Fee split functions --- |
||
| 461 | |||
| 462 |
// Claim split fee if there is staking-reward coming |
||
| 463 |
// and update global index & fee-per-unit variables |
||
| 464 |
/// @dev BO can call this without trigggering a |
||
| 465 |
function syncGlobalAccounting() external {
|
||
| 466 |
√
|
⟳
|
_requireCallerIsBorrowerOperations(); |
| 467 |
√
|
⟳
|
_syncGlobalAccounting(); |
| 468 |
} |
||
| 469 | |||
| 470 |
function _syncGlobalAccounting() internal {
|
||
| 471 |
√
|
⟳
|
(uint256 _oldIndex, uint256 _newIndex) = _readStEthIndex(); |
| 472 |
√
|
⟳
|
_syncStEthIndex(_oldIndex, _newIndex); |
| 473 |
√
|
⟳
|
if (_newIndex > _oldIndex && totalStakes > 0) {
|
| 474 |
( |
||
| 475 |
√
|
⟳
|
uint256 _feeTaken, |
| 476 |
√
|
⟳
|
uint256 _newFeePerUnit, |
| 477 |
√
|
⟳
|
uint256 _perUnitError |
| 478 |
√
|
⟳
|
) = _calcSyncedGlobalAccounting(_newIndex, _oldIndex); |
| 479 |
√
|
⟳
|
_takeSplitAndUpdateFeePerUnit(_feeTaken, _newFeePerUnit, _perUnitError); |
| 480 |
√
|
⟳
|
_updateSystemSnapshotsExcludeCollRemainder(0); |
| 481 |
} |
||
| 482 |
} |
||
| 483 | |||
| 484 |
/// @notice Claim Fee Split, toggles Grace Period accordingly |
||
| 485 |
/// @notice Call this if you want to accrue feeSplit |
||
| 486 |
function syncGlobalAccountingAndGracePeriod() public {
|
||
| 487 |
√
|
⟳
|
_syncGlobalAccounting(); // Apply // Could trigger RM |
| 488 |
√
|
⟳
|
_syncGracePeriod(); // Synch Grace Period |
| 489 |
} |
||
| 490 | |||
| 491 |
/// @return existing(old) local stETH index AND |
||
| 492 |
/// @return current(new) stETH index from collateral token |
||
| 493 |
√
|
⟳
|
function _readStEthIndex() internal view returns (uint256, uint256) {
|
| 494 |
√
|
⟳
|
return (stEthIndex, collateral.getPooledEthByShares(DECIMAL_PRECISION)); |
| 495 |
} |
||
| 496 | |||
| 497 |
// Update the global index via collateral token |
||
| 498 |
function _syncStEthIndex(uint256 _oldIndex, uint256 _newIndex) internal {
|
||
| 499 |
√
|
⟳
|
if (_newIndex != _oldIndex) {
|
| 500 |
√
|
⟳
|
stEthIndex = _newIndex; |
| 501 |
√
|
⟳
|
lastIndexTimestamp = block.timestamp; |
| 502 |
√
|
⟳
|
emit StEthIndexUpdated(_oldIndex, _newIndex, block.timestamp); |
| 503 |
} |
||
| 504 |
} |
||
| 505 | |||
| 506 |
// Calculate fee for given pair of collateral indexes, following are returned values: |
||
| 507 |
// - fee split in collateral token which will be deduced from current total system collateral |
||
| 508 |
// - fee split increase per unit, used to update systemStEthFeePerUnitIndex |
||
| 509 |
// - fee split calculation error, used to update systemStEthFeePerUnitIndexError |
||
| 510 |
function calcFeeUponStakingReward( |
||
| 511 |
uint256 _newIndex, |
||
| 512 |
uint256 _prevIndex |
||
| 513 |
√
|
⟳
|
) public view returns (uint256, uint256, uint256) {
|
| 514 |
√
|
⟳
|
require(_newIndex > _prevIndex, "CDPManager: only take fee with bigger new index"); |
| 515 |
√
|
⟳
|
uint256 deltaIndex = _newIndex - _prevIndex; |
| 516 |
√
|
⟳
|
uint256 deltaIndexFees = (deltaIndex * stakingRewardSplit) / MAX_REWARD_SPLIT; |
| 517 | |||
| 518 |
// we take the fee for all CDPs immediately which is scaled by index precision |
||
| 519 |
√
|
⟳
|
uint256 _deltaFeeSplit = deltaIndexFees * getSystemCollShares(); |
| 520 |
√
|
⟳
|
uint256 _cachedAllStakes = totalStakes; |
| 521 |
// return the values to update the global fee accumulator |
||
| 522 |
√
|
⟳
|
uint256 _feeTaken = collateral.getSharesByPooledEth(_deltaFeeSplit) / DECIMAL_PRECISION; |
| 523 |
√
|
⟳
|
uint256 _deltaFeeSplitShare = (_feeTaken * DECIMAL_PRECISION) + |
| 524 |
√
|
⟳
|
systemStEthFeePerUnitIndexError; |
| 525 |
√
|
⟳
|
uint256 _deltaFeePerUnit = _deltaFeeSplitShare / _cachedAllStakes; |
| 526 |
√
|
⟳
|
uint256 _perUnitError = _deltaFeeSplitShare - (_deltaFeePerUnit * _cachedAllStakes); |
| 527 |
√
|
⟳
|
return (_feeTaken, _deltaFeePerUnit, _perUnitError); |
| 528 |
} |
||
| 529 | |||
| 530 |
// Take the cut from staking reward |
||
| 531 |
// and update global fee-per-unit accumulator |
||
| 532 |
function _takeSplitAndUpdateFeePerUnit( |
||
| 533 |
uint256 _feeTaken, |
||
| 534 |
uint256 _newPerUnit, |
||
| 535 |
uint256 _newErrorPerUnit |
||
| 536 |
) internal {
|
||
| 537 |
√
|
⟳
|
uint256 _oldPerUnit = systemStEthFeePerUnitIndex; |
| 538 | |||
| 539 |
√
|
⟳
|
systemStEthFeePerUnitIndex = _newPerUnit; |
| 540 |
√
|
⟳
|
systemStEthFeePerUnitIndexError = _newErrorPerUnit; |
| 541 | |||
| 542 |
√
|
⟳
|
require(activePool.getSystemCollShares() > _feeTaken, "CDPManager: fee split is too big"); |
| 543 |
√
|
⟳
|
activePool.allocateSystemCollSharesToFeeRecipient(_feeTaken); |
| 544 | |||
| 545 |
√
|
⟳
|
emit CollateralFeePerUnitUpdated(_oldPerUnit, _newPerUnit, _feeTaken); |
| 546 |
} |
||
| 547 | |||
| 548 |
// Apply accumulated fee split distributed to the CDP |
||
| 549 |
// and update its accumulator tracker accordingly |
||
| 550 |
function _applyAccumulatedFeeSplit( |
||
| 551 |
bytes32 _cdpId |
||
| 552 |
√
|
⟳
|
) internal returns (uint256, uint256, uint256, uint256) {
|
| 553 |
// TODO Ensure global states like systemStEthFeePerUnitIndex get timely updated |
||
| 554 |
// whenever there is a CDP modification operation, |
||
| 555 |
// such as opening, closing, adding collateral, repaying debt, or liquidating |
||
| 556 |
// OR Should we utilize some bot-keeper to work the routine job at fixed interval? |
||
| 557 |
√
|
⟳
|
_syncGlobalAccounting(); |
| 558 | |||
| 559 |
√
|
⟳
|
uint256 _oldPerUnitCdp = cdpStEthFeePerUnitIndex[_cdpId]; |
| 560 |
√
|
⟳
|
uint256 _systemStEthFeePerUnitIndex = systemStEthFeePerUnitIndex; |
| 561 | |||
| 562 |
( |
||
| 563 |
√
|
⟳
|
uint256 _newColl, |
| 564 |
√
|
⟳
|
uint256 _newDebt, |
| 565 |
√
|
⟳
|
uint256 _feeSplitDistributed, |
| 566 |
√
|
⟳
|
uint _pendingDebt |
| 567 |
√
|
⟳
|
) = _calcSyncedAccounting(_cdpId, _oldPerUnitCdp, _systemStEthFeePerUnitIndex); |
| 568 | |||
| 569 |
// apply split fee to given CDP |
||
| 570 |
√
|
⟳
|
if (_feeSplitDistributed > 0) {
|
| 571 |
√
|
⟳
|
Cdps[_cdpId].coll = _newColl; |
| 572 | |||
| 573 |
emit CdpFeeSplitApplied( |
||
| 574 |
√
|
⟳
|
_cdpId, |
| 575 |
√
|
⟳
|
_oldPerUnitCdp, |
| 576 |
√
|
⟳
|
_systemStEthFeePerUnitIndex, |
| 577 |
√
|
⟳
|
_feeSplitDistributed, |
| 578 |
√
|
⟳
|
_newColl |
| 579 |
); |
||
| 580 |
} |
||
| 581 | |||
| 582 |
// sync per stake index for given CDP |
||
| 583 |
√
|
⟳
|
if (_oldPerUnitCdp != _systemStEthFeePerUnitIndex) {
|
| 584 |
√
|
⟳
|
cdpStEthFeePerUnitIndex[_cdpId] = _systemStEthFeePerUnitIndex; |
| 585 |
} |
||
| 586 | |||
| 587 |
√
|
⟳
|
return (_newColl, _newDebt, _feeSplitDistributed, _pendingDebt); |
| 588 |
} |
||
| 589 | |||
| 590 |
// return the applied split fee(scaled by 1e18) and the resulting CDP collateral amount after applied |
||
| 591 |
function getAccumulatedFeeSplitApplied( |
||
| 592 |
bytes32 _cdpId, |
||
| 593 |
uint256 _systemStEthFeePerUnitIndex |
||
| 594 |
√
|
⟳
|
) public view returns (uint256, uint256) {
|
| 595 |
√
|
⟳
|
uint256 _cdpStEthFeePerUnitIndex = cdpStEthFeePerUnitIndex[_cdpId]; |
| 596 |
√
|
⟳
|
uint256 _cdpCol = Cdps[_cdpId].coll; |
| 597 | |||
| 598 |
if ( |
||
| 599 |
√
|
⟳
|
_cdpStEthFeePerUnitIndex == 0 || |
| 600 |
√
|
⟳
|
_cdpCol == 0 || |
| 601 |
√
|
⟳
|
_cdpStEthFeePerUnitIndex == _systemStEthFeePerUnitIndex |
| 602 |
) {
|
||
| 603 |
return (0, _cdpCol); |
||
| 604 |
} |
||
| 605 | |||
| 606 |
√
|
⟳
|
uint256 _feeSplitDistributed = Cdps[_cdpId].stake * |
| 607 |
√
|
⟳
|
(_systemStEthFeePerUnitIndex - _cdpStEthFeePerUnitIndex); |
| 608 | |||
| 609 |
√
|
⟳
|
uint256 _scaledCdpColl = _cdpCol * DECIMAL_PRECISION; |
| 610 | |||
| 611 |
√
|
⟳
|
if (_scaledCdpColl > _feeSplitDistributed) {
|
| 612 |
return ( |
||
| 613 |
√
|
⟳
|
_feeSplitDistributed, |
| 614 |
√
|
⟳
|
(_scaledCdpColl - _feeSplitDistributed) / DECIMAL_PRECISION |
| 615 |
); |
||
| 616 |
} else {
|
||
| 617 |
// extreme unlikely case to skip fee split on this CDP to avoid revert |
||
| 618 |
return (0, _cdpCol); |
||
| 619 |
} |
||
| 620 |
} |
||
| 621 | |||
| 622 |
// -- Modifier functions -- |
||
| 623 |
function _requireCdpIsActive(bytes32 _cdpId) internal view {
|
||
| 624 |
√
|
⟳
|
require(Cdps[_cdpId].status == Status.active, "CdpManager: Cdp does not exist or is closed"); |
| 625 |
} |
||
| 626 | |||
| 627 |
function _requireMoreThanOneCdpInSystem(uint256 CdpOwnersArrayLength) internal view {
|
||
| 628 |
require( |
||
| 629 |
⟳
|
CdpOwnersArrayLength > 1 && sortedCdps.getSize() > 1, |
|
| 630 |
"CdpManager: Only one cdp in the system" |
||
| 631 |
); |
||
| 632 |
} |
||
| 633 | |||
| 634 |
function _requireCallerIsBorrowerOperations() internal view {
|
||
| 635 |
require( |
||
| 636 |
√
|
⟳
|
msg.sender == borrowerOperationsAddress, |
| 637 |
"CdpManager: Caller is not the BorrowerOperations contract" |
||
| 638 |
); |
||
| 639 |
} |
||
| 640 | |||
| 641 |
// --- Helper functions --- |
||
| 642 | |||
| 643 |
/// @notice Return the nominal collateral ratio (ICR) of a given Cdp, without the price. |
||
| 644 |
/// @dev Takes a cdp's pending coll and debt rewards from redistributions into account. |
||
| 645 |
√
|
⟳
|
function getNominalICR(bytes32 _cdpId) external view returns (uint256) {
|
| 646 |
√
|
⟳
|
(uint256 currentEBTCDebt, uint256 currentCollShares, ) = getDebtAndCollShares(_cdpId); |
| 647 | |||
| 648 |
√
|
⟳
|
uint256 NICR = EbtcMath._computeNominalCR(currentCollShares, currentEBTCDebt); |
| 649 |
√
|
⟳
|
return NICR; |
| 650 |
} |
||
| 651 | |||
| 652 |
/// @notice Return the nominal collateral ratio (ICR) of a given Cdp, without the price. |
||
| 653 |
/// @dev Takes a cdp's pending coll and debt rewards as well as stETH Index into account. |
||
| 654 |
√
|
function getSyncedNominalICR(bytes32 _cdpId) external view returns (uint256) {
|
|
| 655 |
√
|
(uint256 _oldIndex, uint256 _newIndex) = _readStEthIndex(); |
|
| 656 |
√
|
(, uint256 _newGlobalSplitIdx, ) = _calcSyncedGlobalAccounting(_newIndex, _oldIndex); |
|
| 657 |
√
|
(uint256 _newColl, uint256 _newDebt, , uint256 _pendingDebt) = _calcSyncedAccounting( |
|
| 658 |
√
|
_cdpId, |
|
| 659 |
√
|
cdpStEthFeePerUnitIndex[_cdpId], |
|
| 660 |
√
|
_newGlobalSplitIdx /// NOTE: This is latest index |
|
| 661 |
); |
||
| 662 | |||
| 663 |
√
|
uint256 NICR = EbtcMath._computeNominalCR(_newColl, _newDebt); |
|
| 664 |
√
|
return NICR; |
|
| 665 |
} |
||
| 666 | |||
| 667 |
// Return the current collateral ratio (ICR) of a given Cdp. |
||
| 668 |
//Takes a cdp's pending coll and debt rewards from redistributions into account. |
||
| 669 |
√
|
⟳
|
function getICR(bytes32 _cdpId, uint256 _price) public view returns (uint256) {
|
| 670 |
√
|
⟳
|
(uint256 currentEBTCDebt, uint256 currentCollShares, ) = getDebtAndCollShares(_cdpId); |
| 671 |
√
|
⟳
|
uint256 ICR = _calculateCR(currentCollShares, currentEBTCDebt, _price); |
| 672 |
√
|
⟳
|
return ICR; |
| 673 |
} |
||
| 674 | |||
| 675 |
function _calculateCR( |
||
| 676 |
uint256 currentCollShare, |
||
| 677 |
uint256 currentDebt, |
||
| 678 |
uint256 _price |
||
| 679 |
√
|
⟳
|
) internal view returns (uint256) {
|
| 680 |
√
|
⟳
|
uint256 _underlyingCollateral = collateral.getPooledEthByShares(currentCollShare); |
| 681 |
√
|
⟳
|
return EbtcMath._computeCR(_underlyingCollateral, currentDebt, _price); |
| 682 |
} |
||
| 683 | |||
| 684 |
/** |
||
| 685 |
get the pending Cdp debt "reward" (i.e. the amount of extra debt assigned to the Cdp) from liquidation redistribution events, earned by their stake |
||
| 686 |
*/ |
||
| 687 |
function getPendingRedistributedDebt( |
||
| 688 |
bytes32 _cdpId |
||
| 689 |
) public view returns (uint256 pendingEBTCDebtReward) {
|
||
| 690 |
return _getPendingRedistributedDebt(_cdpId); |
||
| 691 |
} |
||
| 692 | |||
| 693 |
function hasPendingRedistributedDebt(bytes32 _cdpId) public view returns (bool) {
|
||
| 694 |
return _hasRedistributedDebt(_cdpId); |
||
| 695 |
} |
||
| 696 | |||
| 697 |
// Return the Cdps entire debt and coll struct |
||
| 698 |
function _getDebtAndCollShares( |
||
| 699 |
bytes32 _cdpId |
||
| 700 |
√
|
) internal view returns (CdpDebtAndCollShares memory) {
|
|
| 701 |
√
|
(uint256 entireDebt, uint256 entireColl, uint256 pendingDebtReward) = getDebtAndCollShares( |
|
| 702 |
√
|
_cdpId |
|
| 703 |
); |
||
| 704 |
√
|
return CdpDebtAndCollShares(entireDebt, entireColl, pendingDebtReward); |
|
| 705 |
} |
||
| 706 | |||
| 707 |
// Return the Cdps entire debt and coll, including pending rewards from redistributions and collateral reduction from split fee. |
||
| 708 |
/// @notice pending rewards are included in the debt and coll totals returned. |
||
| 709 |
function getDebtAndCollShares( |
||
| 710 |
bytes32 _cdpId |
||
| 711 |
√
|
⟳
|
) public view returns (uint256 debt, uint256 coll, uint256 pendingEBTCDebtReward) {
|
| 712 |
√
|
⟳
|
(uint256 _newColl, uint256 _newDebt, , uint256 _pendingDebt) = _calcSyncedAccounting( |
| 713 |
√
|
⟳
|
_cdpId, |
| 714 |
√
|
⟳
|
cdpStEthFeePerUnitIndex[_cdpId], |
| 715 |
√
|
⟳
|
systemStEthFeePerUnitIndex |
| 716 |
); |
||
| 717 |
√
|
⟳
|
coll = _newColl; |
| 718 |
√
|
⟳
|
debt = _newDebt; |
| 719 |
√
|
⟳
|
pendingEBTCDebtReward = _pendingDebt; |
| 720 |
} |
||
| 721 | |||
| 722 |
/// @dev calculate pending global state change to be applied: |
||
| 723 |
/// @return split fee taken (if any) AND |
||
| 724 |
/// @return new split index per stake unit AND |
||
| 725 |
/// @return new split index error |
||
| 726 |
function _calcSyncedGlobalAccounting( |
||
| 727 |
uint256 _newIndex, |
||
| 728 |
uint256 _oldIndex |
||
| 729 |
√
|
⟳
|
) internal view returns (uint256, uint256, uint256) {
|
| 730 |
√
|
⟳
|
if (_newIndex > _oldIndex && totalStakes > 0) {
|
| 731 |
/// @audit-ok We don't take the fee if we had a negative rebase |
||
| 732 |
( |
||
| 733 |
√
|
⟳
|
uint256 _feeTaken, |
| 734 |
√
|
⟳
|
uint256 _deltaFeePerUnit, |
| 735 |
√
|
⟳
|
uint256 _perUnitError |
| 736 |
√
|
⟳
|
) = calcFeeUponStakingReward(_newIndex, _oldIndex); |
| 737 | |||
| 738 |
// calculate new split per stake unit |
||
| 739 |
√
|
⟳
|
uint256 _newPerUnit = systemStEthFeePerUnitIndex + _deltaFeePerUnit; |
| 740 |
√
|
⟳
|
return (_feeTaken, _newPerUnit, _perUnitError); |
| 741 |
} else {
|
||
| 742 |
√
|
⟳
|
return (0, systemStEthFeePerUnitIndex, systemStEthFeePerUnitIndexError); |
| 743 |
} |
||
| 744 |
} |
||
| 745 | |||
| 746 |
/// @dev calculate pending state change to be applied for given CDP and global split index(typically already synced): |
||
| 747 |
/// @return new CDP collateral share after pending change applied |
||
| 748 |
/// @return new CDP debt after pending change applied |
||
| 749 |
/// @return split fee applied to given CDP |
||
| 750 |
/// @return redistributed debt applied to given CDP |
||
| 751 |
function _calcSyncedAccounting( |
||
| 752 |
bytes32 _cdpId, |
||
| 753 |
uint256 _cdpPerUnitIdx, |
||
| 754 |
uint256 _systemStEthFeePerUnitIndex |
||
| 755 |
√
|
⟳
|
) internal view returns (uint256, uint256, uint256, uint256) {
|
| 756 |
√
|
⟳
|
uint256 _feeSplitApplied; |
| 757 |
√
|
⟳
|
uint256 _newCollShare = Cdps[_cdpId].coll; |
| 758 | |||
| 759 |
// processing split fee to be applied |
||
| 760 |
√
|
⟳
|
if (_cdpPerUnitIdx != _systemStEthFeePerUnitIndex && _cdpPerUnitIdx > 0) {
|
| 761 |
( |
||
| 762 |
√
|
⟳
|
uint256 _feeSplitDistributed, |
| 763 |
√
|
⟳
|
uint256 _newCollShareAfter |
| 764 |
√
|
⟳
|
) = getAccumulatedFeeSplitApplied(_cdpId, _systemStEthFeePerUnitIndex); |
| 765 |
√
|
⟳
|
_feeSplitApplied = _feeSplitDistributed; |
| 766 |
√
|
⟳
|
_newCollShare = _newCollShareAfter; |
| 767 |
} |
||
| 768 | |||
| 769 |
// processing redistributed debt to be applied |
||
| 770 |
√
|
⟳
|
(uint256 _newDebt, uint256 pendingDebtRedistributed) = _getSyncedCdpDebtAndRedistribution( |
| 771 |
√
|
⟳
|
_cdpId |
| 772 |
); |
||
| 773 | |||
| 774 |
√
|
⟳
|
return (_newCollShare, _newDebt, _feeSplitApplied, pendingDebtRedistributed); |
| 775 |
} |
||
| 776 | |||
| 777 |
/// @return CDP debt and pending redistribution from liquidation applied |
||
| 778 |
function _getSyncedCdpDebtAndRedistribution( |
||
| 779 |
bytes32 _cdpId |
||
| 780 |
√
|
⟳
|
) internal view returns (uint256, uint256) {
|
| 781 |
√
|
⟳
|
uint256 pendingDebtRedistributed = _getPendingRedistributedDebt(_cdpId); |
| 782 |
√
|
⟳
|
uint256 _newDebt = Cdps[_cdpId].debt; |
| 783 |
√
|
⟳
|
if (pendingDebtRedistributed > 0) {
|
| 784 |
_newDebt = _newDebt + pendingDebtRedistributed; |
||
| 785 |
} |
||
| 786 |
√
|
⟳
|
return (_newDebt, pendingDebtRedistributed); |
| 787 |
} |
||
| 788 | |||
| 789 |
/// @return CDP debt with pending redistribution from liquidation applied |
||
| 790 |
√
|
⟳
|
function getSyncedCdpDebt(bytes32 _cdpId) public view returns (uint256) {
|
| 791 |
√
|
⟳
|
(uint256 _newDebt, ) = _getSyncedCdpDebtAndRedistribution(_cdpId); |
| 792 |
√
|
⟳
|
return _newDebt; |
| 793 |
} |
||
| 794 | |||
| 795 |
/// @return CDP collateral with pending split fee applied |
||
| 796 |
√
|
⟳
|
function getSyncedCdpCollShares(bytes32 _cdpId) public view returns (uint256) {
|
| 797 |
√
|
⟳
|
(uint256 _oldIndex, uint256 _newIndex) = _readStEthIndex(); |
| 798 |
√
|
⟳
|
(, uint256 _newGlobalSplitIdx, ) = _calcSyncedGlobalAccounting(_newIndex, _oldIndex); |
| 799 |
√
|
⟳
|
(uint256 _newColl, , , ) = _calcSyncedAccounting( |
| 800 |
√
|
⟳
|
_cdpId, |
| 801 |
√
|
⟳
|
cdpStEthFeePerUnitIndex[_cdpId], |
| 802 |
√
|
⟳
|
_newGlobalSplitIdx |
| 803 |
); |
||
| 804 |
√
|
⟳
|
return _newColl; |
| 805 |
} |
||
| 806 | |||
| 807 |
/// @return CDP ICR with pending collateral and debt change applied |
||
| 808 |
√
|
⟳
|
function getSyncedICR(bytes32 _cdpId, uint256 _price) public view returns (uint256) {
|
| 809 |
√
|
⟳
|
uint256 _debt = getSyncedCdpDebt(_cdpId); |
| 810 |
√
|
⟳
|
uint256 _collShare = getSyncedCdpCollShares(_cdpId); |
| 811 |
√
|
⟳
|
return _calculateCR(_collShare, _debt, _price); |
| 812 |
} |
||
| 813 | |||
| 814 |
/// @return TCR with pending collateral and debt change applied |
||
| 815 |
√
|
function getSyncedTCR(uint256 _price) public view returns (uint256) {
|
|
| 816 |
√
|
(uint256 _oldIndex, uint256 _newIndex) = _readStEthIndex(); |
|
| 817 |
√
|
(uint256 _feeTaken, , ) = _calcSyncedGlobalAccounting(_newIndex, _oldIndex); |
|
| 818 | |||
| 819 |
√
|
uint256 _systemCollShare = activePool.getSystemCollShares(); |
|
| 820 |
√
|
if (_feeTaken > 0) {
|
|
| 821 |
√
|
_systemCollShare = _systemCollShare - _feeTaken; |
|
| 822 |
} |
||
| 823 |
√
|
uint256 _systemDebt = activePool.getSystemDebt(); |
|
| 824 |
√
|
return _calculateCR(_systemCollShare, _systemDebt, _price); |
|
| 825 |
} |
||
| 826 | |||
| 827 |
// Can liquidate in RM if ICR < TCR AND Enough time has passed |
||
| 828 |
function canLiquidateRecoveryMode(uint256 icr, uint256 tcr) public view returns (bool) {
|
||
| 829 |
return _checkICRAgainstTCR(icr, tcr) && _recoveryModeGracePeriodPassed(); |
||
| 830 |
} |
||
| 831 | |||
| 832 |
/// @dev Check if enough time has passed for grace period after enabled |
||
| 833 |
function _recoveryModeGracePeriodPassed() internal view returns (bool) {
|
||
| 834 |
// we have waited enough |
||
| 835 |
uint128 cachedLastGracePeriodStartTimestamp = lastGracePeriodStartTimestamp; |
||
| 836 |
return |
||
| 837 |
cachedLastGracePeriodStartTimestamp != UNSET_TIMESTAMP && |
||
| 838 |
block.timestamp > cachedLastGracePeriodStartTimestamp + recoveryModeGracePeriodDuration; |
||
| 839 |
} |
||
| 840 |
} |
||
| 841 |
| Lines covered: | 20 / 42 (47.6%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Interfaces/ICollSurplusPool.sol"; |
||
| 6 |
import "./Dependencies/ICollateralToken.sol"; |
||
| 7 |
import "./Dependencies/SafeERC20.sol"; |
||
| 8 |
import "./Dependencies/ReentrancyGuard.sol"; |
||
| 9 |
import "./Dependencies/AuthNoOwner.sol"; |
||
| 10 |
import "./Interfaces/IActivePool.sol"; |
||
| 11 | |||
| 12 |
contract CollSurplusPool is ICollSurplusPool, ReentrancyGuard, AuthNoOwner {
|
||
| 13 |
using SafeERC20 for IERC20; |
||
| 14 | |||
| 15 |
string public constant NAME = "CollSurplusPool"; |
||
| 16 | |||
| 17 |
address public immutable borrowerOperationsAddress; |
||
| 18 |
address public immutable cdpManagerAddress; |
||
| 19 |
address public immutable activePoolAddress; |
||
| 20 |
address public immutable feeRecipientAddress; |
||
| 21 |
ICollateralToken public immutable collateral; |
||
| 22 | |||
| 23 |
// deposited ether tracker |
||
| 24 |
uint256 internal totalSurplusCollShares; |
||
| 25 |
// Collateral surplus claimable by cdp owners |
||
| 26 |
mapping(address => uint256) internal balances; |
||
| 27 | |||
| 28 |
// --- Contract setters --- |
||
| 29 | |||
| 30 |
/** |
||
| 31 |
* @notice Sets the addresses of the contracts and renounces ownership |
||
| 32 |
* @dev One-time initialization function. Can only be called by the owner as a security measure. Ownership is renounced after the function is called. |
||
| 33 |
* @param _borrowerOperationsAddress The address of the BorrowerOperations |
||
| 34 |
* @param _cdpManagerAddress The address of the CDPManager |
||
| 35 |
* @param _activePoolAddress The address of the ActivePool |
||
| 36 |
* @param _collTokenAddress The address of the CollateralToken |
||
| 37 |
*/ |
||
| 38 |
constructor( |
||
| 39 |
address _borrowerOperationsAddress, |
||
| 40 |
address _cdpManagerAddress, |
||
| 41 |
address _activePoolAddress, |
||
| 42 |
address _collTokenAddress |
||
| 43 |
) {
|
||
| 44 |
√
|
borrowerOperationsAddress = _borrowerOperationsAddress; |
|
| 45 |
√
|
cdpManagerAddress = _cdpManagerAddress; |
|
| 46 |
√
|
activePoolAddress = _activePoolAddress; |
|
| 47 |
√
|
collateral = ICollateralToken(_collTokenAddress); |
|
| 48 |
√
|
feeRecipientAddress = IActivePool(activePoolAddress).feeRecipientAddress(); |
|
| 49 | |||
| 50 |
√
|
address _authorityAddress = address(AuthNoOwner(cdpManagerAddress).authority()); |
|
| 51 |
√
|
if (_authorityAddress != address(0)) {
|
|
| 52 |
√
|
_initializeAuthority(_authorityAddress); |
|
| 53 |
} |
||
| 54 |
} |
||
| 55 | |||
| 56 |
/** |
||
| 57 |
* @notice Gets the current collateral state variable of the pool |
||
| 58 |
* @dev Not necessarily equal to the raw collateral token balance - tokens can be forcibly sent to contracts |
||
| 59 |
* @return The current collateral balance tracked by the variable |
||
| 60 |
*/ |
||
| 61 |
√
|
⟳
|
function getTotalSurplusCollShares() external view override returns (uint256) {
|
| 62 |
√
|
⟳
|
return totalSurplusCollShares; |
| 63 |
} |
||
| 64 | |||
| 65 |
/** |
||
| 66 |
* @notice Gets the collateral surplus available for the given account |
||
| 67 |
* @param _account The address of the account |
||
| 68 |
* @return The collateral balance available to claim |
||
| 69 |
*/ |
||
| 70 |
√
|
function getSurplusCollShares(address _account) external view override returns (uint256) {
|
|
| 71 |
√
|
return balances[_account]; |
|
| 72 |
} |
||
| 73 | |||
| 74 |
// --- Pool functionality --- |
||
| 75 | |||
| 76 |
function increaseSurplusCollShares(address _account, uint256 _amount) external override {
|
||
| 77 |
⟳
|
_requireCallerIsCdpManager(); |
|
| 78 | |||
| 79 |
⟳
|
uint256 newAmount = balances[_account] + _amount; |
|
| 80 |
⟳
|
balances[_account] = newAmount; |
|
| 81 | |||
| 82 |
⟳
|
emit SurplusCollSharesUpdated(_account, newAmount); |
|
| 83 |
} |
||
| 84 | |||
| 85 |
function claimSurplusCollShares(address _account) external override {
|
||
| 86 |
_requireCallerIsBorrowerOperations(); |
||
| 87 |
uint256 claimableColl = balances[_account]; |
||
| 88 |
require(claimableColl > 0, "CollSurplusPool: No collateral available to claim"); |
||
| 89 | |||
| 90 |
balances[_account] = 0; |
||
| 91 |
emit SurplusCollSharesUpdated(_account, 0); |
||
| 92 | |||
| 93 |
uint256 cachedTotalSurplusCollShares = totalSurplusCollShares; |
||
| 94 | |||
| 95 |
require(cachedTotalSurplusCollShares >= claimableColl, "!CollSurplusPoolBal"); |
||
| 96 |
// Safe per the check above |
||
| 97 |
unchecked {
|
||
| 98 |
totalSurplusCollShares = cachedTotalSurplusCollShares - claimableColl; |
||
| 99 |
} |
||
| 100 |
emit CollSharesTransferred(_account, claimableColl); |
||
| 101 | |||
| 102 |
// NOTE: No need for safe transfer if the collateral asset is standard. Make sure this is the case! |
||
| 103 |
collateral.transferShares(_account, claimableColl); |
||
| 104 |
} |
||
| 105 | |||
| 106 |
// --- 'require' functions --- |
||
| 107 | |||
| 108 |
function _requireCallerIsBorrowerOperations() internal view {
|
||
| 109 |
require( |
||
| 110 |
msg.sender == borrowerOperationsAddress, |
||
| 111 |
"CollSurplusPool: Caller is not Borrower Operations" |
||
| 112 |
); |
||
| 113 |
} |
||
| 114 | |||
| 115 |
function _requireCallerIsCdpManager() internal view {
|
||
| 116 |
⟳
|
require(msg.sender == cdpManagerAddress, "CollSurplusPool: Caller is not CdpManager"); |
|
| 117 |
} |
||
| 118 | |||
| 119 |
function _requireCallerIsActivePool() internal view {
|
||
| 120 |
⟳
|
require(msg.sender == activePoolAddress, "CollSurplusPool: Caller is not Active Pool"); |
|
| 121 |
} |
||
| 122 | |||
| 123 |
function increaseTotalSurplusCollShares(uint256 _value) external override {
|
||
| 124 |
⟳
|
_requireCallerIsActivePool(); |
|
| 125 |
⟳
|
totalSurplusCollShares = totalSurplusCollShares + _value; |
|
| 126 |
} |
||
| 127 | |||
| 128 |
// === Governed Functions === // |
||
| 129 | |||
| 130 |
/// @dev Function to move unintended dust that are not protected |
||
| 131 |
/// @notice moves given amount of given token (collateral is NOT allowed) |
||
| 132 |
/// @notice because recipient are fixed, this function is safe to be called by anyone |
||
| 133 |
function sweepToken(address token, uint256 amount) public nonReentrant requiresAuth {
|
||
| 134 |
require(token != address(collateral), "CollSurplusPool: Cannot Sweep Collateral"); |
||
| 135 | |||
| 136 |
uint256 balance = IERC20(token).balanceOf(address(this)); |
||
| 137 |
require(amount <= balance, "CollSurplusPool: Attempt to sweep more than balance"); |
||
| 138 | |||
| 139 |
IERC20(token).safeTransfer(feeRecipientAddress, amount); |
||
| 140 | |||
| 141 |
emit SweepTokenSuccess(token, amount, feeRecipientAddress); |
||
| 142 |
} |
||
| 143 |
} |
||
| 144 |
| Lines covered: | 0 / 19 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v4.8.0) (utils/Address.sol) |
||
| 3 | |||
| 4 |
pragma solidity 0.8.17; |
||
| 5 | |||
| 6 |
/** |
||
| 7 |
* @dev Collection of functions related to the address type |
||
| 8 |
*/ |
||
| 9 |
library Address {
|
||
| 10 |
/** |
||
| 11 |
* @dev Returns true if `account` is a contract. |
||
| 12 |
* |
||
| 13 |
* [IMPORTANT] |
||
| 14 |
* ==== |
||
| 15 |
* It is unsafe to assume that an address for which this function returns |
||
| 16 |
* false is an externally-owned account (EOA) and not a contract. |
||
| 17 |
* |
||
| 18 |
* Among others, `isContract` will return false for the following |
||
| 19 |
* types of addresses: |
||
| 20 |
* |
||
| 21 |
* - an externally-owned account |
||
| 22 |
* - a contract in construction |
||
| 23 |
* - an address where a contract will be created |
||
| 24 |
* - an address where a contract lived, but was destroyed |
||
| 25 |
* |
||
| 26 |
* Furthermore, `isContract` will also return true if the target contract within |
||
| 27 |
* the same transaction is already scheduled for destruction by `SELFDESTRUCT`, |
||
| 28 |
* which only has an effect at the end of a transaction. |
||
| 29 |
* ==== |
||
| 30 |
* |
||
| 31 |
* [IMPORTANT] |
||
| 32 |
* ==== |
||
| 33 |
* You shouldn't rely on `isContract` to protect against flash loan attacks! |
||
| 34 |
* |
||
| 35 |
* Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets |
||
| 36 |
* like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract |
||
| 37 |
* constructor. |
||
| 38 |
* ==== |
||
| 39 |
*/ |
||
| 40 |
function isContract(address account) internal view returns (bool) {
|
||
| 41 |
// This method relies on extcodesize/address.code.length, which returns 0 |
||
| 42 |
// for contracts in construction, since the code is only stored at the end |
||
| 43 |
// of the constructor execution. |
||
| 44 | |||
| 45 |
return account.code.length > 0; |
||
| 46 |
} |
||
| 47 | |||
| 48 |
/** |
||
| 49 |
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
|
||
| 50 |
* `errorMessage` as a fallback revert reason when `target` reverts. |
||
| 51 |
* |
||
| 52 |
* _Available since v3.1._ |
||
| 53 |
*/ |
||
| 54 |
function functionCall( |
||
| 55 |
address target, |
||
| 56 |
bytes memory data, |
||
| 57 |
string memory errorMessage |
||
| 58 |
) internal returns (bytes memory) {
|
||
| 59 |
return functionCallWithValue(target, data, 0, errorMessage); |
||
| 60 |
} |
||
| 61 | |||
| 62 |
/** |
||
| 63 |
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
|
||
| 64 |
* with `errorMessage` as a fallback revert reason when `target` reverts. |
||
| 65 |
* |
||
| 66 |
* _Available since v3.1._ |
||
| 67 |
*/ |
||
| 68 |
function functionCallWithValue( |
||
| 69 |
address target, |
||
| 70 |
bytes memory data, |
||
| 71 |
uint256 value, |
||
| 72 |
string memory errorMessage |
||
| 73 |
) internal returns (bytes memory) {
|
||
| 74 |
require(address(this).balance >= value, "Address: insufficient balance for call"); |
||
| 75 |
(bool success, bytes memory returndata) = target.call{value: value}(data);
|
||
| 76 |
return verifyCallResultFromTarget(target, success, returndata, errorMessage); |
||
| 77 |
} |
||
| 78 | |||
| 79 |
/** |
||
| 80 |
* @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling |
||
| 81 |
* the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. |
||
| 82 |
* |
||
| 83 |
* _Available since v4.8._ |
||
| 84 |
*/ |
||
| 85 |
function verifyCallResultFromTarget( |
||
| 86 |
address target, |
||
| 87 |
bool success, |
||
| 88 |
bytes memory returndata, |
||
| 89 |
string memory errorMessage |
||
| 90 |
) internal view returns (bytes memory) {
|
||
| 91 |
if (success) {
|
||
| 92 |
if (returndata.length == 0) {
|
||
| 93 |
// only check isContract if the call was successful and the return data is empty |
||
| 94 |
// otherwise we already know that it was a contract |
||
| 95 |
require(isContract(target), "Address: call to non-contract"); |
||
| 96 |
} |
||
| 97 |
return returndata; |
||
| 98 |
} else {
|
||
| 99 |
_revert(returndata, errorMessage); |
||
| 100 |
} |
||
| 101 |
} |
||
| 102 | |||
| 103 |
function _revert(bytes memory returndata, string memory errorMessage) private pure {
|
||
| 104 |
// Look for revert reason and bubble it up if present |
||
| 105 |
if (returndata.length > 0) {
|
||
| 106 |
// The easiest way to bubble the revert reason is using memory via assembly |
||
| 107 |
/// @solidity memory-safe-assembly |
||
| 108 |
assembly {
|
||
| 109 |
let returndata_size := mload(returndata) |
||
| 110 |
revert(add(32, returndata), returndata_size) |
||
| 111 |
} |
||
| 112 |
} else {
|
||
| 113 |
revert(errorMessage); |
||
| 114 |
} |
||
| 115 |
} |
||
| 116 |
} |
||
| 117 |
| Lines covered: | 9 / 16 (56.2%) |
|---|
| 1 |
// SPDX-License-Identifier: AGPL-3.0-only |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
import {Authority} from "./Authority.sol";
|
||
| 5 | |||
| 6 |
/// @notice Provides a flexible and updatable auth pattern which is completely separate from application logic. |
||
| 7 |
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/Auth.sol) |
||
| 8 |
/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) |
||
| 9 |
abstract contract Auth {
|
||
| 10 |
event OwnershipTransferred(address indexed user, address indexed newOwner); |
||
| 11 | |||
| 12 |
event AuthorityUpdated(address indexed user, Authority indexed newAuthority); |
||
| 13 | |||
| 14 |
address public owner; |
||
| 15 | |||
| 16 |
Authority public authority; |
||
| 17 | |||
| 18 |
constructor(address _owner, Authority _authority) {
|
||
| 19 |
√
|
owner = _owner; |
|
| 20 |
√
|
authority = _authority; |
|
| 21 | |||
| 22 |
√
|
emit OwnershipTransferred(msg.sender, _owner); |
|
| 23 |
√
|
emit AuthorityUpdated(msg.sender, _authority); |
|
| 24 |
} |
||
| 25 | |||
| 26 |
modifier requiresAuth() virtual {
|
||
| 27 |
√
|
require(isAuthorized(msg.sender, msg.sig), "Auth: UNAUTHORIZED"); |
|
| 28 | |||
| 29 |
_; |
||
| 30 |
} |
||
| 31 | |||
| 32 |
√
|
function isAuthorized(address user, bytes4 functionSig) internal view virtual returns (bool) {
|
|
| 33 |
√
|
Authority auth = authority; // Memoizing authority saves us a warm SLOAD, around 100 gas. |
|
| 34 | |||
| 35 |
// Checking if the caller is the owner only after calling the authority saves gas in most cases, but be |
||
| 36 |
// aware that this makes protected functions uncallable even to the owner if the authority is out of order. |
||
| 37 |
return |
||
| 38 |
√
|
(address(auth) != address(0) && auth.canCall(user, address(this), functionSig)) || |
|
| 39 |
√
|
user == owner; |
|
| 40 |
} |
||
| 41 | |||
| 42 |
function setAuthority(Authority newAuthority) public virtual {
|
||
| 43 |
// We check if the caller is the owner first because we want to ensure they can |
||
| 44 |
// always swap out the authority even if it's reverting or using up a lot of gas. |
||
| 45 |
require(msg.sender == owner || authority.canCall(msg.sender, address(this), msg.sig)); |
||
| 46 | |||
| 47 |
authority = newAuthority; |
||
| 48 | |||
| 49 |
emit AuthorityUpdated(msg.sender, newAuthority); |
||
| 50 |
} |
||
| 51 | |||
| 52 |
function transferOwnership(address newOwner) public virtual requiresAuth {
|
||
| 53 |
owner = newOwner; |
||
| 54 | |||
| 55 |
emit OwnershipTransferred(msg.sender, newOwner); |
||
| 56 |
} |
||
| 57 |
} |
||
| 58 |
| Lines covered: | 11 / 19 (57.9%) |
|---|
| 1 |
// SPDX-License-Identifier: AGPL-3.0-only |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
import {Authority} from "./Authority.sol";
|
||
| 5 | |||
| 6 |
/// @notice Provides a flexible and updatable auth pattern which is completely separate from application logic. |
||
| 7 |
/// @author Modified by BadgerDAO to remove owner |
||
| 8 |
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/Auth.sol) |
||
| 9 |
/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) |
||
| 10 |
contract AuthNoOwner {
|
||
| 11 |
event AuthorityUpdated(address indexed user, Authority indexed newAuthority); |
||
| 12 | |||
| 13 |
Authority private _authority; |
||
| 14 |
bool private _authorityInitialized; |
||
| 15 | |||
| 16 |
modifier requiresAuth() virtual {
|
||
| 17 |
√
|
⟳
|
require(isAuthorized(msg.sender, msg.sig), "Auth: UNAUTHORIZED"); |
| 18 | |||
| 19 |
_; |
||
| 20 |
} |
||
| 21 | |||
| 22 |
√
|
function authority() public view returns (Authority) {
|
|
| 23 |
√
|
return _authority; |
|
| 24 |
} |
||
| 25 | |||
| 26 |
function authorityInitialized() public view returns (bool) {
|
||
| 27 |
return _authorityInitialized; |
||
| 28 |
} |
||
| 29 | |||
| 30 |
√
|
⟳
|
function isAuthorized(address user, bytes4 functionSig) internal view virtual returns (bool) {
|
| 31 |
√
|
⟳
|
Authority auth = _authority; // Memoizing authority saves us a warm SLOAD, around 100 gas. |
| 32 | |||
| 33 |
// Checking if the caller is the owner only after calling the authority saves gas in most cases, but be |
||
| 34 |
// aware that this makes protected functions uncallable even to the owner if the authority is out of order. |
||
| 35 |
√
|
⟳
|
return (address(auth) != address(0) && auth.canCall(user, address(this), functionSig)); |
| 36 |
} |
||
| 37 | |||
| 38 |
function setAuthority(address newAuthority) public virtual {
|
||
| 39 |
// We check if the caller is the owner first because we want to ensure they can |
||
| 40 |
// always swap out the authority even if it's reverting or using up a lot of gas. |
||
| 41 |
require(_authority.canCall(msg.sender, address(this), msg.sig)); |
||
| 42 | |||
| 43 |
_authority = Authority(newAuthority); |
||
| 44 | |||
| 45 |
// Once authority is set once via any means, ensure it is initialized |
||
| 46 |
if (!_authorityInitialized) {
|
||
| 47 |
_authorityInitialized = true; |
||
| 48 |
} |
||
| 49 | |||
| 50 |
emit AuthorityUpdated(msg.sender, Authority(newAuthority)); |
||
| 51 |
} |
||
| 52 | |||
| 53 |
/// @notice Changed constructor to initialize to allow flexiblity of constructor vs initializer use |
||
| 54 |
/// @notice sets authorityInitiailzed flag to ensure only one use of |
||
| 55 |
function _initializeAuthority(address newAuthority) internal {
|
||
| 56 |
√
|
require(address(_authority) == address(0), "Auth: authority is non-zero"); |
|
| 57 |
√
|
require(!_authorityInitialized, "Auth: authority already initialized"); |
|
| 58 | |||
| 59 |
√
|
_authority = Authority(newAuthority); |
|
| 60 |
√
|
_authorityInitialized = true; |
|
| 61 | |||
| 62 |
√
|
emit AuthorityUpdated(address(this), Authority(newAuthority)); |
|
| 63 |
} |
||
| 64 |
} |
||
| 65 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: AGPL-3.0-only |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
/// @notice A generic interface for a contract which provides authorization data to an Auth instance. |
||
| 5 |
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/Auth.sol) |
||
| 6 |
/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) |
||
| 7 |
interface Authority {
|
||
| 8 |
function canCall(address user, address target, bytes4 functionSig) external view returns (bool); |
||
| 9 |
} |
||
| 10 |
| Lines covered: | 1 / 2 (50.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
contract BaseMath {
|
||
| 5 |
√
|
⟳
|
uint256 public constant DECIMAL_PRECISION = 1e18; |
| 6 |
} |
||
| 7 |
| Lines covered: | 2 / 2 (100.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) |
||
| 3 | |||
| 4 |
pragma solidity 0.8.17; |
||
| 5 | |||
| 6 |
/** |
||
| 7 |
* @dev Provides information about the current execution context, including the |
||
| 8 |
* sender of the transaction and its data. While these are generally available |
||
| 9 |
* via msg.sender and msg.data, they should not be accessed in such a direct |
||
| 10 |
* manner, since when dealing with meta-transactions the account sending and |
||
| 11 |
* paying for execution may not be the actual sender (as far as an application |
||
| 12 |
* is concerned). |
||
| 13 |
* |
||
| 14 |
* This contract is only required for intermediate, library-like contracts. |
||
| 15 |
*/ |
||
| 16 |
abstract contract Context {
|
||
| 17 |
√
|
function _msgSender() internal view virtual returns (address) {
|
|
| 18 |
√
|
return msg.sender; |
|
| 19 |
} |
||
| 20 | |||
| 21 |
function _msgData() internal view virtual returns (bytes calldata) {
|
||
| 22 |
return msg.data; |
||
| 23 |
} |
||
| 24 |
} |
||
| 25 |
| Lines covered: | 20 / 21 (95.2%) |
|---|
| 1 |
//SPDX-License-Identifier: Unlicense |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
/** |
||
| 5 |
@title A library for deploying contracts EIP-3171 style. |
||
| 6 |
@author Agustin Aguilar <aa@horizon.io> |
||
| 7 |
*/ |
||
| 8 |
library Create3 {
|
||
| 9 |
error ErrorCreatingProxy(); |
||
| 10 |
error ErrorCreatingContract(); |
||
| 11 |
error TargetAlreadyExists(); |
||
| 12 | |||
| 13 |
/** |
||
| 14 |
@notice The bytecode for a contract that proxies the creation of another contract |
||
| 15 |
@dev If this code is deployed using CREATE2 it can be used to decouple `creationCode` from the child contract address |
||
| 16 | |||
| 17 |
0x67363d3d37363d34f03d5260086018f3: |
||
| 18 |
0x00 0x67 0x67XXXXXXXXXXXXXXXX PUSH8 bytecode 0x363d3d37363d34f0 |
||
| 19 |
0x01 0x3d 0x3d RETURNDATASIZE 0 0x363d3d37363d34f0 |
||
| 20 |
0x02 0x52 0x52 MSTORE |
||
| 21 |
0x03 0x60 0x6008 PUSH1 08 8 |
||
| 22 |
0x04 0x60 0x6018 PUSH1 18 24 8 |
||
| 23 |
0x05 0xf3 0xf3 RETURN |
||
| 24 | |||
| 25 |
0x363d3d37363d34f0: |
||
| 26 |
0x00 0x36 0x36 CALLDATASIZE cds |
||
| 27 |
0x01 0x3d 0x3d RETURNDATASIZE 0 cds |
||
| 28 |
0x02 0x3d 0x3d RETURNDATASIZE 0 0 cds |
||
| 29 |
0x03 0x37 0x37 CALLDATACOPY |
||
| 30 |
0x04 0x36 0x36 CALLDATASIZE cds |
||
| 31 |
0x05 0x3d 0x3d RETURNDATASIZE 0 cds |
||
| 32 |
0x06 0x34 0x34 CALLVALUE val 0 cds |
||
| 33 |
0x07 0xf0 0xf0 CREATE addr |
||
| 34 |
*/ |
||
| 35 | |||
| 36 |
bytes internal constant PROXY_CHILD_BYTECODE = |
||
| 37 |
hex"67_36_3d_3d_37_36_3d_34_f0_3d_52_60_08_60_18_f3"; |
||
| 38 | |||
| 39 |
// KECCAK256_PROXY_CHILD_BYTECODE = keccak256(PROXY_CHILD_BYTECODE); |
||
| 40 |
bytes32 internal constant KECCAK256_PROXY_CHILD_BYTECODE = |
||
| 41 |
√
|
0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f; |
|
| 42 | |||
| 43 |
/** |
||
| 44 |
@notice Returns the size of the code on a given address |
||
| 45 |
@param _addr Address that may or may not contain code |
||
| 46 |
@return size of the code on the given `_addr` |
||
| 47 |
*/ |
||
| 48 |
√
|
function codeSize(address _addr) internal view returns (uint256 size) {
|
|
| 49 |
assembly {
|
||
| 50 |
√
|
size := extcodesize(_addr) |
|
| 51 |
} |
||
| 52 |
} |
||
| 53 | |||
| 54 |
/** |
||
| 55 |
@notice Creates a new contract with given `_creationCode` and `_salt` |
||
| 56 |
@param _salt Salt of the contract creation, resulting address will be derivated from this value only |
||
| 57 |
@param _creationCode Creation code (constructor) of the contract to be deployed, this value doesn't affect the resulting address |
||
| 58 |
@return addr of the deployed contract, reverts on error |
||
| 59 |
*/ |
||
| 60 |
√
|
function create3(bytes32 _salt, bytes memory _creationCode) internal returns (address addr) {
|
|
| 61 |
√
|
return create3(_salt, _creationCode, 0); |
|
| 62 |
} |
||
| 63 | |||
| 64 |
/** |
||
| 65 |
@notice Creates a new contract with given `_creationCode` and `_salt` |
||
| 66 |
@param _salt Salt of the contract creation, resulting address will be derivated from this value only |
||
| 67 |
@param _creationCode Creation code (constructor) of the contract to be deployed, this value doesn't affect the resulting address |
||
| 68 |
@param _value In WEI of ETH to be forwarded to child contract |
||
| 69 |
@return addr of the deployed contract, reverts on error |
||
| 70 |
*/ |
||
| 71 |
function create3( |
||
| 72 |
bytes32 _salt, |
||
| 73 |
bytes memory _creationCode, |
||
| 74 |
uint256 _value |
||
| 75 |
√
|
) internal returns (address addr) {
|
|
| 76 |
// Creation code |
||
| 77 |
√
|
bytes memory creationCode = PROXY_CHILD_BYTECODE; |
|
| 78 | |||
| 79 |
// Get target final address |
||
| 80 |
√
|
addr = addressOf(_salt); |
|
| 81 |
√
|
if (codeSize(addr) != 0) revert TargetAlreadyExists(); |
|
| 82 | |||
| 83 |
// Create CREATE2 proxy |
||
| 84 |
√
|
address proxy; |
|
| 85 |
assembly {
|
||
| 86 |
√
|
proxy := create2(0, add(creationCode, 32), mload(creationCode), _salt) |
|
| 87 |
} |
||
| 88 |
√
|
if (proxy == address(0)) revert ErrorCreatingProxy(); |
|
| 89 | |||
| 90 |
// Call proxy with final init code |
||
| 91 |
√
|
(bool success, ) = proxy.call{value: _value}(_creationCode);
|
|
| 92 |
√
|
if (!success || codeSize(addr) == 0) revert ErrorCreatingContract(); |
|
| 93 |
} |
||
| 94 | |||
| 95 |
/** |
||
| 96 |
@notice Computes the resulting address of a contract deployed using address(this) and the given `_salt` |
||
| 97 |
@param _salt Salt of the contract creation, resulting address will be derivated from this value only |
||
| 98 |
@return addr of the deployed contract, reverts on error |
||
| 99 | |||
| 100 |
@dev The address creation formula is: keccak256(rlp([keccak256(0xff ++ address(this) ++ _salt ++ keccak256(childBytecode))[12:], 0x01])) |
||
| 101 |
*/ |
||
| 102 |
√
|
function addressOf(bytes32 _salt) internal view returns (address) {
|
|
| 103 |
√
|
address proxy = address( |
|
| 104 |
uint160( |
||
| 105 |
uint256( |
||
| 106 |
keccak256( |
||
| 107 |
abi.encodePacked( |
||
| 108 |
hex"ff", |
||
| 109 |
√
|
address(this), |
|
| 110 |
√
|
_salt, |
|
| 111 |
√
|
KECCAK256_PROXY_CHILD_BYTECODE |
|
| 112 |
) |
||
| 113 |
) |
||
| 114 |
) |
||
| 115 |
) |
||
| 116 |
); |
||
| 117 | |||
| 118 |
√
|
return address(uint160(uint256(keccak256(abi.encodePacked(hex"d6_94", proxy, hex"01"))))); |
|
| 119 |
} |
||
| 120 |
} |
||
| 121 |
| Lines covered: | 1 / 5 (20.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "../Interfaces/IERC3156FlashLender.sol"; |
||
| 6 |
import "../Interfaces/IWETH.sol"; |
||
| 7 | |||
| 8 |
abstract contract ERC3156FlashLender is IERC3156FlashLender {
|
||
| 9 |
// TODO: Fix / Finalize |
||
| 10 |
uint256 public constant MAX_BPS = 10_000; |
||
| 11 |
uint256 public constant MAX_FEE_BPS = 1_000; // 10% |
||
| 12 |
bytes32 public constant FLASH_SUCCESS_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
|
||
| 13 | |||
| 14 |
// Functions to modify these variables must be included in impelemnting contracts if desired |
||
| 15 |
√
|
uint16 public feeBps = 3; // may be subject to future adjustments through protocol governance |
|
| 16 |
bool public flashLoansPaused; |
||
| 17 |
} |
||
| 18 |
| Lines covered: | 39 / 44 (88.6%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./BaseMath.sol"; |
||
| 6 |
import "./EbtcMath.sol"; |
||
| 7 |
import "../Interfaces/IActivePool.sol"; |
||
| 8 |
import "../Interfaces/IPriceFeed.sol"; |
||
| 9 |
import "../Interfaces/IEbtcBase.sol"; |
||
| 10 |
import "../Dependencies/ICollateralToken.sol"; |
||
| 11 | |||
| 12 |
/* |
||
| 13 |
* Base contract for CdpManager, BorrowerOperations. Contains global system constants and |
||
| 14 |
* common functions. |
||
| 15 |
*/ |
||
| 16 |
contract EbtcBase is BaseMath, IEbtcBase {
|
||
| 17 |
// Collateral Ratio applied for Liquidation Incentive |
||
| 18 |
// i.e., liquidator repay $1 worth of debt to get back $1.03 worth of collateral |
||
| 19 |
√
|
uint256 public constant LICR = 1030000000000000000; // 103% |
|
| 20 | |||
| 21 |
// Minimum collateral ratio for individual cdps |
||
| 22 |
√
|
⟳
|
uint256 public constant MCR = 1100000000000000000; // 110% |
| 23 | |||
| 24 |
// Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, Recovery Mode is triggered. |
||
| 25 |
√
|
⟳
|
uint256 public constant CCR = 1250000000000000000; // 125% |
| 26 | |||
| 27 |
// Amount of stETH collateral to be locked in active pool on opening cdps |
||
| 28 |
√
|
⟳
|
uint256 public constant LIQUIDATOR_REWARD = 2e17; |
| 29 | |||
| 30 |
// Minimum amount of stETH collateral a CDP must have |
||
| 31 |
√
|
⟳
|
uint256 public constant MIN_NET_COLL = 2e18; |
| 32 | |||
| 33 |
uint256 public constant PERCENT_DIVISOR = 200; // dividing by 200 yields 0.5% |
||
| 34 | |||
| 35 |
uint256 public constant BORROWING_FEE_FLOOR = 0; // 0.5% |
||
| 36 | |||
| 37 |
√
|
uint256 public constant STAKING_REWARD_SPLIT = 5_000; // taking 50% cut from staking reward |
|
| 38 | |||
| 39 |
√
|
⟳
|
uint256 public constant MAX_REWARD_SPLIT = 10_000; |
| 40 | |||
| 41 |
IActivePool public immutable activePool; |
||
| 42 | |||
| 43 |
IPriceFeed public immutable override priceFeed; |
||
| 44 | |||
| 45 |
// the only collateral token allowed in CDP |
||
| 46 |
ICollateralToken public immutable collateral; |
||
| 47 | |||
| 48 |
constructor(address _activePoolAddress, address _priceFeedAddress, address _collateralAddress) {
|
||
| 49 |
√
|
activePool = IActivePool(_activePoolAddress); |
|
| 50 |
√
|
priceFeed = IPriceFeed(_priceFeedAddress); |
|
| 51 |
√
|
collateral = ICollateralToken(_collateralAddress); |
|
| 52 |
} |
||
| 53 | |||
| 54 |
// --- Gas compensation functions --- |
||
| 55 | |||
| 56 |
√
|
⟳
|
function _getNetColl(uint256 _coll) internal pure returns (uint256) {
|
| 57 |
√
|
⟳
|
return _coll - LIQUIDATOR_REWARD; |
| 58 |
} |
||
| 59 | |||
| 60 |
/** |
||
| 61 |
@notice Get the entire system collateral |
||
| 62 |
@notice Entire system collateral = collateral stored in ActivePool, using their internal accounting |
||
| 63 |
@dev Coll stored for liquidator rewards or coll in CollSurplusPool are not included |
||
| 64 |
*/ |
||
| 65 |
√
|
⟳
|
function getSystemCollShares() public view returns (uint256 entireSystemColl) {
|
| 66 |
√
|
⟳
|
return (activePool.getSystemCollShares()); |
| 67 |
} |
||
| 68 | |||
| 69 |
/** |
||
| 70 |
@notice Get the entire system debt |
||
| 71 |
@notice Entire system collateral = collateral stored in ActivePool, using their internal accounting |
||
| 72 |
*/ |
||
| 73 |
√
|
⟳
|
function _getSystemDebt() internal view returns (uint256 entireSystemDebt) {
|
| 74 |
√
|
⟳
|
return (activePool.getSystemDebt()); |
| 75 |
} |
||
| 76 | |||
| 77 |
√
|
⟳
|
function _getTCR(uint256 _price) internal view returns (uint256 TCR) {
|
| 78 |
√
|
⟳
|
(TCR, , ) = _getTCRWithSystemDebtAndCollShares(_price); |
| 79 |
} |
||
| 80 | |||
| 81 |
function _getTCRWithSystemDebtAndCollShares( |
||
| 82 |
uint256 _price |
||
| 83 |
√
|
⟳
|
) internal view returns (uint256 TCR, uint256 _coll, uint256 _debt) {
|
| 84 |
√
|
⟳
|
uint256 systemCollShares = getSystemCollShares(); |
| 85 |
√
|
⟳
|
uint256 systemDebt = _getSystemDebt(); |
| 86 | |||
| 87 |
√
|
⟳
|
uint256 _systemStEthBalance = collateral.getPooledEthByShares(systemCollShares); |
| 88 |
√
|
⟳
|
TCR = EbtcMath._computeCR(_systemStEthBalance, systemDebt, _price); |
| 89 | |||
| 90 |
√
|
⟳
|
return (TCR, systemCollShares, systemDebt); |
| 91 |
} |
||
| 92 | |||
| 93 |
√
|
⟳
|
function _checkRecoveryMode(uint256 _price) internal view returns (bool) {
|
| 94 |
√
|
⟳
|
return _checkRecoveryModeForTCR(_getTCR(_price)); |
| 95 |
} |
||
| 96 | |||
| 97 |
√
|
⟳
|
function _checkRecoveryModeForTCR(uint256 _tcr) internal view returns (bool) {
|
| 98 |
√
|
⟳
|
return _tcr < CCR; |
| 99 |
} |
||
| 100 | |||
| 101 |
function _requireUserAcceptsFee( |
||
| 102 |
uint256 _fee, |
||
| 103 |
uint256 _amount, |
||
| 104 |
uint256 _maxFeePercentage |
||
| 105 |
) internal pure {
|
||
| 106 |
⟳
|
uint256 feePercentage = (_fee * DECIMAL_PRECISION) / _amount; |
|
| 107 |
⟳
|
require(feePercentage <= _maxFeePercentage, "Fee exceeded provided maximum"); |
|
| 108 |
} |
||
| 109 | |||
| 110 |
// Convert debt denominated in ETH to debt denominated in BTC given that _price is ETH/BTC |
||
| 111 |
// _debt is denominated in ETH |
||
| 112 |
// _price is ETH/BTC |
||
| 113 |
function _convertDebtDenominationToBtc( |
||
| 114 |
uint256 _debt, |
||
| 115 |
uint256 _price |
||
| 116 |
√
|
) internal pure returns (uint256) {
|
|
| 117 |
√
|
return (_debt * _price) / DECIMAL_PRECISION; |
|
| 118 |
} |
||
| 119 | |||
| 120 |
/// @dev return true if given ICR is qualified for liquidation compared to configured threshold |
||
| 121 |
/// @dev this function ONLY checks numbers not check grace period switch for Recovery Mode |
||
| 122 |
√
|
⟳
|
function _checkICRAgainstLiqThreshold(uint256 _icr, uint _tcr) internal view returns (bool) {
|
| 123 |
// Either undercollateralized |
||
| 124 |
// OR, it's RM AND they meet the requirement |
||
| 125 |
// Swapped Requirement && RM to save gas |
||
| 126 |
return |
||
| 127 |
√
|
⟳
|
_checkICRAgainstMCR(_icr) || |
| 128 |
√
|
⟳
|
(_checkICRAgainstTCR(_icr, _tcr) && _checkRecoveryModeForTCR(_tcr)); |
| 129 |
} |
||
| 130 | |||
| 131 |
/// @dev return true if given ICR is qualified for liquidation compared to MCR |
||
| 132 |
√
|
⟳
|
function _checkICRAgainstMCR(uint256 _icr) internal view returns (bool) {
|
| 133 |
√
|
⟳
|
return _icr < MCR; |
| 134 |
} |
||
| 135 | |||
| 136 |
/// @dev return true if given ICR is qualified for liquidation compared to TCR |
||
| 137 |
/// @dev typically used in Recovery Mode |
||
| 138 |
√
|
⟳
|
function _checkICRAgainstTCR(uint256 _icr, uint _tcr) internal view returns (bool) {
|
| 139 |
/// @audit is _icr <= _tcr more dangerous for overal system safety? |
||
| 140 |
/// @audit Should we use _icr < CCR to allow any risky CDP being liquidated? |
||
| 141 |
√
|
⟳
|
return _icr <= _tcr; |
| 142 |
} |
||
| 143 |
} |
||
| 144 |
| Lines covered: | 31 / 36 (86.1%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
library EbtcMath {
|
||
| 6 |
√
|
⟳
|
uint256 internal constant DECIMAL_PRECISION = 1e18; |
| 7 |
√
|
⟳
|
uint256 public constant MAX_TCR = type(uint256).max; |
| 8 | |||
| 9 |
/* Precision for Nominal ICR (independent of price). Rationale for the value: |
||
| 10 |
* |
||
| 11 |
* - Making it “too high” could lead to overflows. |
||
| 12 |
* - Making it “too low” could lead to an ICR equal to zero, due to truncation from Solidity floor division. |
||
| 13 |
* |
||
| 14 |
* This value of 1e20 is chosen for safety: the NICR will only overflow for numerator > ~1e39 ETH, |
||
| 15 |
* and will only truncate to 0 if the denominator is at least 1e20 times greater than the numerator. |
||
| 16 |
* |
||
| 17 |
*/ |
||
| 18 |
√
|
⟳
|
uint256 internal constant NICR_PRECISION = 1e20; |
| 19 | |||
| 20 |
⟳
|
function _min(uint256 _a, uint256 _b) internal pure returns (uint256) {
|
|
| 21 |
⟳
|
return (_a < _b) ? _a : _b; |
|
| 22 |
} |
||
| 23 | |||
| 24 |
function _max(uint256 _a, uint256 _b) internal pure returns (uint256) {
|
||
| 25 |
return (_a >= _b) ? _a : _b; |
||
| 26 |
} |
||
| 27 | |||
| 28 |
/* |
||
| 29 |
* Multiply two decimal numbers and use normal rounding rules: |
||
| 30 |
* -round product up if 19'th mantissa digit >= 5 |
||
| 31 |
* -round product down if 19'th mantissa digit < 5 |
||
| 32 |
* |
||
| 33 |
* Used only inside the exponentiation, _decPow(). |
||
| 34 |
*/ |
||
| 35 |
√
|
⟳
|
function decMul(uint256 x, uint256 y) internal pure returns (uint256 decProd) {
|
| 36 |
√
|
⟳
|
uint256 prod_xy = x * y; |
| 37 | |||
| 38 |
√
|
⟳
|
decProd = (prod_xy + (DECIMAL_PRECISION / 2)) / DECIMAL_PRECISION; |
| 39 |
} |
||
| 40 | |||
| 41 |
/* |
||
| 42 |
* _decPow: Exponentiation function for 18-digit decimal base, and integer exponent n. |
||
| 43 |
* |
||
| 44 |
* Uses the efficient "exponentiation by squaring" algorithm. O(log(n)) complexity. |
||
| 45 |
* |
||
| 46 |
* Called by two functions that represent time in units of minutes: |
||
| 47 |
* 1) CdpManager._calcDecayedBaseRate |
||
| 48 |
* 2) CommunityIssuance._getCumulativeIssuanceFraction |
||
| 49 |
* |
||
| 50 |
* The exponent is capped to avoid reverting due to overflow. The cap 525600000 equals |
||
| 51 |
* "minutes in 1000 years": 60 * 24 * 365 * 1000 |
||
| 52 |
* |
||
| 53 |
* If a period of > 1000 years is ever used as an exponent in either of the above functions, the result will be |
||
| 54 |
* negligibly different from just passing the cap, since: |
||
| 55 |
* |
||
| 56 |
* In function 1), the decayed base rate will be 0 for 1000 years or > 1000 years |
||
| 57 |
* In function 2), the difference in tokens issued at 1000 years and any time > 1000 years, will be negligible |
||
| 58 |
*/ |
||
| 59 |
√
|
⟳
|
function _decPow(uint256 _base, uint256 _minutes) internal pure returns (uint256) {
|
| 60 |
√
|
⟳
|
if (_minutes > 525600000) {
|
| 61 |
_minutes = 525600000; |
||
| 62 |
} // cap to avoid overflow |
||
| 63 | |||
| 64 |
√
|
⟳
|
if (_minutes == 0) {
|
| 65 |
return DECIMAL_PRECISION; |
||
| 66 |
} |
||
| 67 | |||
| 68 |
√
|
⟳
|
uint256 y = DECIMAL_PRECISION; |
| 69 |
√
|
⟳
|
uint256 x = _base; |
| 70 |
√
|
⟳
|
uint256 n = _minutes; |
| 71 | |||
| 72 |
// Exponentiation-by-squaring |
||
| 73 |
√
|
⟳
|
while (n > 1) {
|
| 74 |
√
|
⟳
|
if (n % 2 == 0) {
|
| 75 |
√
|
⟳
|
x = decMul(x, x); |
| 76 |
√
|
⟳
|
n = n / 2; |
| 77 |
} else {
|
||
| 78 |
// if (n % 2 != 0) |
||
| 79 |
√
|
⟳
|
y = decMul(x, y); |
| 80 |
√
|
⟳
|
x = decMul(x, x); |
| 81 |
√
|
⟳
|
n = (n - 1) / 2; |
| 82 |
} |
||
| 83 |
} |
||
| 84 | |||
| 85 |
√
|
⟳
|
return decMul(x, y); |
| 86 |
} |
||
| 87 | |||
| 88 |
function _getAbsoluteDifference(uint256 _a, uint256 _b) internal pure returns (uint256) {
|
||
| 89 |
return (_a >= _b) ? (_a - _b) : (_b - _a); |
||
| 90 |
} |
||
| 91 | |||
| 92 |
√
|
⟳
|
function _computeNominalCR(uint256 _collShares, uint256 _debt) internal pure returns (uint256) {
|
| 93 |
√
|
⟳
|
if (_debt > 0) {
|
| 94 |
√
|
⟳
|
return (_collShares * NICR_PRECISION) / _debt; |
| 95 |
} |
||
| 96 |
// Return the maximal value for uint256 if the Cdp has a debt of 0. Represents "infinite" CR. |
||
| 97 |
else {
|
||
| 98 |
// if (_debt == 0) |
||
| 99 |
√
|
⟳
|
return MAX_TCR; |
| 100 |
} |
||
| 101 |
} |
||
| 102 | |||
| 103 |
/// @dev Compute collateralization ratio, given stETH balance, price, and debt balance |
||
| 104 |
function _computeCR( |
||
| 105 |
uint256 _stEthBalance, |
||
| 106 |
uint256 _debt, |
||
| 107 |
uint256 _price |
||
| 108 |
√
|
⟳
|
) internal pure returns (uint256) {
|
| 109 |
√
|
⟳
|
if (_debt > 0) {
|
| 110 |
√
|
⟳
|
uint256 newCollRatio = (_stEthBalance * _price) / _debt; |
| 111 | |||
| 112 |
√
|
⟳
|
return newCollRatio; |
| 113 |
} |
||
| 114 |
// Return the maximal value for uint256 if the Cdp has a debt of 0. Represents "infinite" CR. |
||
| 115 |
else {
|
||
| 116 |
// if (_debt == 0) |
||
| 117 |
√
|
⟳
|
return MAX_TCR; |
| 118 |
} |
||
| 119 |
} |
||
| 120 |
} |
||
| 121 |
| Lines covered: | 13 / 54 (24.1%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v4.8.0) (utils/structs/EnumerableSet.sol) |
||
| 3 |
// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js. |
||
| 4 | |||
| 5 |
pragma solidity 0.8.17; |
||
| 6 | |||
| 7 |
/** |
||
| 8 |
* @dev Library for managing |
||
| 9 |
* https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive |
||
| 10 |
* types. |
||
| 11 |
* |
||
| 12 |
* Sets have the following properties: |
||
| 13 |
* |
||
| 14 |
* - Elements are added, removed, and checked for existence in constant time |
||
| 15 |
* (O(1)). |
||
| 16 |
* - Elements are enumerated in O(n). No guarantees are made on the ordering. |
||
| 17 |
* |
||
| 18 |
* ```solidity |
||
| 19 |
* contract Example {
|
||
| 20 |
* // Add the library methods |
||
| 21 |
* using EnumerableSet for EnumerableSet.AddressSet; |
||
| 22 |
* |
||
| 23 |
* // Declare a set state variable |
||
| 24 |
* EnumerableSet.AddressSet private mySet; |
||
| 25 |
* } |
||
| 26 |
* ``` |
||
| 27 |
* |
||
| 28 |
* As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) |
||
| 29 |
* and `uint256` (`UintSet`) are supported. |
||
| 30 |
* |
||
| 31 |
* [WARNING] |
||
| 32 |
* ==== |
||
| 33 |
* Trying to delete such a structure from storage will likely result in data corruption, rendering the structure |
||
| 34 |
* unusable. |
||
| 35 |
* See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. |
||
| 36 |
* |
||
| 37 |
* In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an |
||
| 38 |
* array of EnumerableSet. |
||
| 39 |
* ==== |
||
| 40 |
*/ |
||
| 41 |
library EnumerableSet {
|
||
| 42 |
// To implement this library for multiple types with as little code |
||
| 43 |
// repetition as possible, we write it in terms of a generic Set type with |
||
| 44 |
// bytes32 values. |
||
| 45 |
// The Set implementation uses private functions, and user-facing |
||
| 46 |
// implementations (such as AddressSet) are just wrappers around the |
||
| 47 |
// underlying Set. |
||
| 48 |
// This means that we can only create new EnumerableSets for types that fit |
||
| 49 |
// in bytes32. |
||
| 50 | |||
| 51 |
struct Set {
|
||
| 52 |
// Storage of set values |
||
| 53 |
bytes32[] _values; |
||
| 54 |
// Position of the value in the `values` array, plus 1 because index 0 |
||
| 55 |
// means a value is not in the set. |
||
| 56 |
mapping(bytes32 => uint256) _indexes; |
||
| 57 |
} |
||
| 58 | |||
| 59 |
/** |
||
| 60 |
* @dev Add a value to a set. O(1). |
||
| 61 |
* |
||
| 62 |
* Returns true if the value was added to the set, that is if it was not |
||
| 63 |
* already present. |
||
| 64 |
*/ |
||
| 65 |
√
|
function _add(Set storage set, bytes32 value) private returns (bool) {
|
|
| 66 |
√
|
if (!_contains(set, value)) {
|
|
| 67 |
√
|
set._values.push(value); |
|
| 68 |
// The value is stored at length-1, but we add 1 to all indexes |
||
| 69 |
// and use 0 as a sentinel value |
||
| 70 |
√
|
set._indexes[value] = set._values.length; |
|
| 71 |
√
|
return true; |
|
| 72 |
} else {
|
||
| 73 |
return false; |
||
| 74 |
} |
||
| 75 |
} |
||
| 76 | |||
| 77 |
/** |
||
| 78 |
* @dev Removes a value from a set. O(1). |
||
| 79 |
* |
||
| 80 |
* Returns true if the value was removed from the set, that is if it was |
||
| 81 |
* present. |
||
| 82 |
*/ |
||
| 83 |
function _remove(Set storage set, bytes32 value) private returns (bool) {
|
||
| 84 |
// We read and store the value's index to prevent multiple reads from the same storage slot |
||
| 85 |
uint256 valueIndex = set._indexes[value]; |
||
| 86 | |||
| 87 |
if (valueIndex != 0) {
|
||
| 88 |
// Equivalent to contains(set, value) |
||
| 89 |
// To delete an element from the _values array in O(1), we swap the element to delete with the last one in |
||
| 90 |
// the array, and then remove the last element (sometimes called as 'swap and pop'). |
||
| 91 |
// This modifies the order of the array, as noted in {at}.
|
||
| 92 | |||
| 93 |
uint256 toDeleteIndex = valueIndex - 1; |
||
| 94 |
uint256 lastIndex = set._values.length - 1; |
||
| 95 | |||
| 96 |
if (lastIndex != toDeleteIndex) {
|
||
| 97 |
bytes32 lastValue = set._values[lastIndex]; |
||
| 98 | |||
| 99 |
// Move the last value to the index where the value to delete is |
||
| 100 |
set._values[toDeleteIndex] = lastValue; |
||
| 101 |
// Update the index for the moved value |
||
| 102 |
set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex |
||
| 103 |
} |
||
| 104 | |||
| 105 |
// Delete the slot where the moved value was stored |
||
| 106 |
set._values.pop(); |
||
| 107 | |||
| 108 |
// Delete the index for the deleted slot |
||
| 109 |
delete set._indexes[value]; |
||
| 110 | |||
| 111 |
return true; |
||
| 112 |
} else {
|
||
| 113 |
return false; |
||
| 114 |
} |
||
| 115 |
} |
||
| 116 | |||
| 117 |
/** |
||
| 118 |
* @dev Returns true if the value is in the set. O(1). |
||
| 119 |
*/ |
||
| 120 |
√
|
function _contains(Set storage set, bytes32 value) private view returns (bool) {
|
|
| 121 |
√
|
return set._indexes[value] != 0; |
|
| 122 |
} |
||
| 123 | |||
| 124 |
/** |
||
| 125 |
* @dev Returns the number of values on the set. O(1). |
||
| 126 |
*/ |
||
| 127 |
function _length(Set storage set) private view returns (uint256) {
|
||
| 128 |
return set._values.length; |
||
| 129 |
} |
||
| 130 | |||
| 131 |
/** |
||
| 132 |
* @dev Returns the value stored at position `index` in the set. O(1). |
||
| 133 |
* |
||
| 134 |
* Note that there are no guarantees on the ordering of values inside the |
||
| 135 |
* array, and it may change when more values are added or removed. |
||
| 136 |
* |
||
| 137 |
* Requirements: |
||
| 138 |
* |
||
| 139 |
* - `index` must be strictly less than {length}.
|
||
| 140 |
*/ |
||
| 141 |
function _at(Set storage set, uint256 index) private view returns (bytes32) {
|
||
| 142 |
return set._values[index]; |
||
| 143 |
} |
||
| 144 | |||
| 145 |
/** |
||
| 146 |
* @dev Return the entire set in an array |
||
| 147 |
* |
||
| 148 |
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed |
||
| 149 |
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that |
||
| 150 |
* this function has an unbounded cost, and using it as part of a state-changing function may render the function |
||
| 151 |
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. |
||
| 152 |
*/ |
||
| 153 |
function _values(Set storage set) private view returns (bytes32[] memory) {
|
||
| 154 |
return set._values; |
||
| 155 |
} |
||
| 156 | |||
| 157 |
// Bytes32Set |
||
| 158 | |||
| 159 |
struct Bytes32Set {
|
||
| 160 |
Set _inner; |
||
| 161 |
} |
||
| 162 | |||
| 163 |
/** |
||
| 164 |
* @dev Add a value to a set. O(1). |
||
| 165 |
* |
||
| 166 |
* Returns true if the value was added to the set, that is if it was not |
||
| 167 |
* already present. |
||
| 168 |
*/ |
||
| 169 |
√
|
function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
|
|
| 170 |
√
|
return _add(set._inner, value); |
|
| 171 |
} |
||
| 172 | |||
| 173 |
/** |
||
| 174 |
* @dev Removes a value from a set. O(1). |
||
| 175 |
* |
||
| 176 |
* Returns true if the value was removed from the set, that is if it was |
||
| 177 |
* present. |
||
| 178 |
*/ |
||
| 179 |
function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
|
||
| 180 |
return _remove(set._inner, value); |
||
| 181 |
} |
||
| 182 | |||
| 183 |
/** |
||
| 184 |
* @dev Returns true if the value is in the set. O(1). |
||
| 185 |
*/ |
||
| 186 |
function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
|
||
| 187 |
return _contains(set._inner, value); |
||
| 188 |
} |
||
| 189 | |||
| 190 |
/** |
||
| 191 |
* @dev Returns the number of values in the set. O(1). |
||
| 192 |
*/ |
||
| 193 |
function length(Bytes32Set storage set) internal view returns (uint256) {
|
||
| 194 |
return _length(set._inner); |
||
| 195 |
} |
||
| 196 | |||
| 197 |
/** |
||
| 198 |
* @dev Returns the value stored at position `index` in the set. O(1). |
||
| 199 |
* |
||
| 200 |
* Note that there are no guarantees on the ordering of values inside the |
||
| 201 |
* array, and it may change when more values are added or removed. |
||
| 202 |
* |
||
| 203 |
* Requirements: |
||
| 204 |
* |
||
| 205 |
* - `index` must be strictly less than {length}.
|
||
| 206 |
*/ |
||
| 207 |
function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
|
||
| 208 |
return _at(set._inner, index); |
||
| 209 |
} |
||
| 210 | |||
| 211 |
/** |
||
| 212 |
* @dev Return the entire set in an array |
||
| 213 |
* |
||
| 214 |
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed |
||
| 215 |
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that |
||
| 216 |
* this function has an unbounded cost, and using it as part of a state-changing function may render the function |
||
| 217 |
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. |
||
| 218 |
*/ |
||
| 219 |
function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
|
||
| 220 |
bytes32[] memory store = _values(set._inner); |
||
| 221 |
bytes32[] memory result; |
||
| 222 | |||
| 223 |
/// @solidity memory-safe-assembly |
||
| 224 |
assembly {
|
||
| 225 |
result := store |
||
| 226 |
} |
||
| 227 | |||
| 228 |
return result; |
||
| 229 |
} |
||
| 230 | |||
| 231 |
// AddressSet |
||
| 232 | |||
| 233 |
struct AddressSet {
|
||
| 234 |
Set _inner; |
||
| 235 |
} |
||
| 236 | |||
| 237 |
/** |
||
| 238 |
* @dev Add a value to a set. O(1). |
||
| 239 |
* |
||
| 240 |
* Returns true if the value was added to the set, that is if it was not |
||
| 241 |
* already present. |
||
| 242 |
*/ |
||
| 243 |
√
|
function add(AddressSet storage set, address value) internal returns (bool) {
|
|
| 244 |
√
|
return _add(set._inner, bytes32(uint256(uint160(value)))); |
|
| 245 |
} |
||
| 246 | |||
| 247 |
/** |
||
| 248 |
* @dev Removes a value from a set. O(1). |
||
| 249 |
* |
||
| 250 |
* Returns true if the value was removed from the set, that is if it was |
||
| 251 |
* present. |
||
| 252 |
*/ |
||
| 253 |
function remove(AddressSet storage set, address value) internal returns (bool) {
|
||
| 254 |
return _remove(set._inner, bytes32(uint256(uint160(value)))); |
||
| 255 |
} |
||
| 256 | |||
| 257 |
/** |
||
| 258 |
* @dev Returns true if the value is in the set. O(1). |
||
| 259 |
*/ |
||
| 260 |
√
|
function contains(AddressSet storage set, address value) internal view returns (bool) {
|
|
| 261 |
√
|
return _contains(set._inner, bytes32(uint256(uint160(value)))); |
|
| 262 |
} |
||
| 263 | |||
| 264 |
/** |
||
| 265 |
* @dev Returns the number of values in the set. O(1). |
||
| 266 |
*/ |
||
| 267 |
function length(AddressSet storage set) internal view returns (uint256) {
|
||
| 268 |
return _length(set._inner); |
||
| 269 |
} |
||
| 270 | |||
| 271 |
/** |
||
| 272 |
* @dev Returns the value stored at position `index` in the set. O(1). |
||
| 273 |
* |
||
| 274 |
* Note that there are no guarantees on the ordering of values inside the |
||
| 275 |
* array, and it may change when more values are added or removed. |
||
| 276 |
* |
||
| 277 |
* Requirements: |
||
| 278 |
* |
||
| 279 |
* - `index` must be strictly less than {length}.
|
||
| 280 |
*/ |
||
| 281 |
function at(AddressSet storage set, uint256 index) internal view returns (address) {
|
||
| 282 |
return address(uint160(uint256(_at(set._inner, index)))); |
||
| 283 |
} |
||
| 284 | |||
| 285 |
/** |
||
| 286 |
* @dev Return the entire set in an array |
||
| 287 |
* |
||
| 288 |
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed |
||
| 289 |
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that |
||
| 290 |
* this function has an unbounded cost, and using it as part of a state-changing function may render the function |
||
| 291 |
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. |
||
| 292 |
*/ |
||
| 293 |
function values(AddressSet storage set) internal view returns (address[] memory) {
|
||
| 294 |
bytes32[] memory store = _values(set._inner); |
||
| 295 |
address[] memory result; |
||
| 296 | |||
| 297 |
/// @solidity memory-safe-assembly |
||
| 298 |
assembly {
|
||
| 299 |
result := store |
||
| 300 |
} |
||
| 301 | |||
| 302 |
return result; |
||
| 303 |
} |
||
| 304 | |||
| 305 |
// UintSet |
||
| 306 | |||
| 307 |
struct UintSet {
|
||
| 308 |
Set _inner; |
||
| 309 |
} |
||
| 310 | |||
| 311 |
/** |
||
| 312 |
* @dev Add a value to a set. O(1). |
||
| 313 |
* |
||
| 314 |
* Returns true if the value was added to the set, that is if it was not |
||
| 315 |
* already present. |
||
| 316 |
*/ |
||
| 317 |
function add(UintSet storage set, uint256 value) internal returns (bool) {
|
||
| 318 |
return _add(set._inner, bytes32(value)); |
||
| 319 |
} |
||
| 320 | |||
| 321 |
/** |
||
| 322 |
* @dev Removes a value from a set. O(1). |
||
| 323 |
* |
||
| 324 |
* Returns true if the value was removed from the set, that is if it was |
||
| 325 |
* present. |
||
| 326 |
*/ |
||
| 327 |
function remove(UintSet storage set, uint256 value) internal returns (bool) {
|
||
| 328 |
return _remove(set._inner, bytes32(value)); |
||
| 329 |
} |
||
| 330 | |||
| 331 |
/** |
||
| 332 |
* @dev Returns true if the value is in the set. O(1). |
||
| 333 |
*/ |
||
| 334 |
function contains(UintSet storage set, uint256 value) internal view returns (bool) {
|
||
| 335 |
return _contains(set._inner, bytes32(value)); |
||
| 336 |
} |
||
| 337 | |||
| 338 |
/** |
||
| 339 |
* @dev Returns the number of values in the set. O(1). |
||
| 340 |
*/ |
||
| 341 |
function length(UintSet storage set) internal view returns (uint256) {
|
||
| 342 |
return _length(set._inner); |
||
| 343 |
} |
||
| 344 | |||
| 345 |
/** |
||
| 346 |
* @dev Returns the value stored at position `index` in the set. O(1). |
||
| 347 |
* |
||
| 348 |
* Note that there are no guarantees on the ordering of values inside the |
||
| 349 |
* array, and it may change when more values are added or removed. |
||
| 350 |
* |
||
| 351 |
* Requirements: |
||
| 352 |
* |
||
| 353 |
* - `index` must be strictly less than {length}.
|
||
| 354 |
*/ |
||
| 355 |
function at(UintSet storage set, uint256 index) internal view returns (uint256) {
|
||
| 356 |
return uint256(_at(set._inner, index)); |
||
| 357 |
} |
||
| 358 | |||
| 359 |
/** |
||
| 360 |
* @dev Return the entire set in an array |
||
| 361 |
* |
||
| 362 |
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed |
||
| 363 |
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that |
||
| 364 |
* this function has an unbounded cost, and using it as part of a state-changing function may render the function |
||
| 365 |
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. |
||
| 366 |
*/ |
||
| 367 |
function values(UintSet storage set) internal view returns (uint256[] memory) {
|
||
| 368 |
bytes32[] memory store = _values(set._inner); |
||
| 369 |
uint256[] memory result; |
||
| 370 | |||
| 371 |
/// @solidity memory-safe-assembly |
||
| 372 |
assembly {
|
||
| 373 |
result := store |
||
| 374 |
} |
||
| 375 | |||
| 376 |
return result; |
||
| 377 |
} |
||
| 378 |
} |
||
| 379 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
import "./IERC20.sol"; |
||
| 5 | |||
| 6 |
/** |
||
| 7 |
* Based on the stETH: |
||
| 8 |
* - https://docs.lido.fi/contracts/lido# |
||
| 9 |
*/ |
||
| 10 |
interface ICollateralToken is IERC20 {
|
||
| 11 |
// Returns the amount of shares that corresponds to _ethAmount protocol-controlled Ether |
||
| 12 |
function getSharesByPooledEth(uint256 _ethAmount) external view returns (uint256); |
||
| 13 | |||
| 14 |
// Returns the amount of Ether that corresponds to _sharesAmount token shares |
||
| 15 |
function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256); |
||
| 16 | |||
| 17 |
// Moves `_sharesAmount` token shares from the caller's account to the `_recipient` account. |
||
| 18 |
function transferShares(address _recipient, uint256 _sharesAmount) external returns (uint256); |
||
| 19 | |||
| 20 |
// Returns the amount of shares owned by _account |
||
| 21 |
function sharesOf(address _account) external view returns (uint256); |
||
| 22 | |||
| 23 |
// Returns authorized oracle address |
||
| 24 |
function getOracle() external view returns (address); |
||
| 25 |
} |
||
| 26 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
/** |
||
| 5 |
* Based on the stETH: |
||
| 6 |
* - https://docs.lido.fi/contracts/lido# |
||
| 7 |
*/ |
||
| 8 |
interface ICollateralTokenOracle {
|
||
| 9 |
// Return beacon specification data. |
||
| 10 |
function getBeaconSpec() |
||
| 11 |
external |
||
| 12 |
view |
||
| 13 |
returns ( |
||
| 14 |
uint64 epochsPerFrame, |
||
| 15 |
uint64 slotsPerEpoch, |
||
| 16 |
uint64 secondsPerSlot, |
||
| 17 |
uint64 genesisTime |
||
| 18 |
); |
||
| 19 |
} |
||
| 20 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
/** |
||
| 6 |
* Based on the OpenZeppelin IER20 interface: |
||
| 7 |
* https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol |
||
| 8 |
* |
||
| 9 |
* @dev Interface of the ERC20 standard as defined in the EIP. |
||
| 10 |
*/ |
||
| 11 |
interface IERC20 {
|
||
| 12 |
/** |
||
| 13 |
* @dev Returns the amount of tokens in existence. |
||
| 14 |
*/ |
||
| 15 |
function totalSupply() external view returns (uint256); |
||
| 16 | |||
| 17 |
/** |
||
| 18 |
* @dev Returns the amount of tokens owned by `account`. |
||
| 19 |
*/ |
||
| 20 |
function balanceOf(address account) external view returns (uint256); |
||
| 21 | |||
| 22 |
/** |
||
| 23 |
* @dev Moves `amount` tokens from the caller's account to `recipient`. |
||
| 24 |
* |
||
| 25 |
* Returns a boolean value indicating whether the operation succeeded. |
||
| 26 |
* |
||
| 27 |
* Emits a {Transfer} event.
|
||
| 28 |
*/ |
||
| 29 |
function transfer(address recipient, uint256 amount) external returns (bool); |
||
| 30 | |||
| 31 |
/** |
||
| 32 |
* @dev Returns the remaining number of tokens that `spender` will be |
||
| 33 |
* allowed to spend on behalf of `owner` through {transferFrom}. This is
|
||
| 34 |
* zero by default. |
||
| 35 |
* |
||
| 36 |
* This value changes when {approve} or {transferFrom} are called.
|
||
| 37 |
*/ |
||
| 38 |
function allowance(address owner, address spender) external view returns (uint256); |
||
| 39 | |||
| 40 |
function increaseAllowance(address spender, uint256 addedValue) external returns (bool); |
||
| 41 | |||
| 42 |
function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); |
||
| 43 | |||
| 44 |
/** |
||
| 45 |
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens. |
||
| 46 |
* |
||
| 47 |
* Returns a boolean value indicating whether the operation succeeded. |
||
| 48 |
* |
||
| 49 |
* IMPORTANT: Beware that changing an allowance with this method brings the risk |
||
| 50 |
* that someone may use both the old and the new allowance by unfortunate |
||
| 51 |
* transaction ordering. One possible solution to mitigate this race |
||
| 52 |
* condition is to first reduce the spender's allowance to 0 and set the |
||
| 53 |
* desired value afterwards: |
||
| 54 |
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 |
||
| 55 |
* |
||
| 56 |
* Emits an {Approval} event.
|
||
| 57 |
*/ |
||
| 58 |
function approve(address spender, uint256 amount) external returns (bool); |
||
| 59 | |||
| 60 |
/** |
||
| 61 |
* @dev Moves `amount` tokens from `sender` to `recipient` using the |
||
| 62 |
* allowance mechanism. `amount` is then deducted from the caller's |
||
| 63 |
* allowance. |
||
| 64 |
* |
||
| 65 |
* Returns a boolean value indicating whether the operation succeeded. |
||
| 66 |
* |
||
| 67 |
* Emits a {Transfer} event.
|
||
| 68 |
*/ |
||
| 69 |
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); |
||
| 70 | |||
| 71 |
function name() external view returns (string memory); |
||
| 72 | |||
| 73 |
function symbol() external view returns (string memory); |
||
| 74 | |||
| 75 |
function decimals() external view returns (uint8); |
||
| 76 | |||
| 77 |
/** |
||
| 78 |
* @dev Emitted when `value` tokens are moved from one account (`from`) to |
||
| 79 |
* another (`to`). |
||
| 80 |
* |
||
| 81 |
* Note that `value` may be zero. |
||
| 82 |
*/ |
||
| 83 |
event Transfer(address indexed from, address indexed to, uint256 value); |
||
| 84 | |||
| 85 |
/** |
||
| 86 |
* @dev Emitted when the allowance of a `spender` for an `owner` is set by |
||
| 87 |
* a call to {approve}. `value` is the new allowance.
|
||
| 88 |
*/ |
||
| 89 |
event Approval(address indexed owner, address indexed spender, uint256 value); |
||
| 90 |
} |
||
| 91 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
/** |
||
| 6 |
* @dev Interface of the ERC2612 standard as defined in the EIP. |
||
| 7 |
* |
||
| 8 |
* Adds the {permit} method, which can be used to change one's
|
||
| 9 |
* {IERC20-allowance} without having to send a transaction, by signing a
|
||
| 10 |
* message. This allows users to spend tokens without having to hold Ether. |
||
| 11 |
* |
||
| 12 |
* See https://eips.ethereum.org/EIPS/eip-2612. |
||
| 13 |
* |
||
| 14 |
* Code adapted from https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2237/ |
||
| 15 |
*/ |
||
| 16 |
interface IERC2612 {
|
||
| 17 |
/** |
||
| 18 |
* @dev Sets `amount` as the allowance of `spender` over `owner`'s tokens, |
||
| 19 |
* given `owner`'s signed approval. |
||
| 20 |
* |
||
| 21 |
* IMPORTANT: The same issues {IERC20-approve} has related to transaction
|
||
| 22 |
* ordering also apply here. |
||
| 23 |
* |
||
| 24 |
* Emits an {Approval} event.
|
||
| 25 |
* |
||
| 26 |
* Requirements: |
||
| 27 |
* |
||
| 28 |
* - `owner` cannot be the zero address. |
||
| 29 |
* - `spender` cannot be the zero address. |
||
| 30 |
* - `deadline` must be a timestamp in the future. |
||
| 31 |
* - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` |
||
| 32 |
* over the EIP712-formatted function arguments. |
||
| 33 |
* - the signature must use ``owner``'s current nonce (see {nonces}).
|
||
| 34 |
* |
||
| 35 |
* For more information on the signature format, see the |
||
| 36 |
* https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP |
||
| 37 |
* section]. |
||
| 38 |
*/ |
||
| 39 |
function permit( |
||
| 40 |
address owner, |
||
| 41 |
address spender, |
||
| 42 |
uint256 amount, |
||
| 43 |
uint256 deadline, |
||
| 44 |
uint8 v, |
||
| 45 |
bytes32 r, |
||
| 46 |
bytes32 s |
||
| 47 |
) external; |
||
| 48 | |||
| 49 |
/** |
||
| 50 |
* @dev Returns the current ERC2612 nonce for `owner`. This value must be |
||
| 51 |
* included whenever a signature is generated for {permit}.
|
||
| 52 |
* |
||
| 53 |
* Every successful call to {permit} increases `owner`'s nonce by one. This
|
||
| 54 |
* prevents a signature from being used multiple times. |
||
| 55 |
* |
||
| 56 |
* `owner` can limit the time a Permit is valid for by setting `deadline` to |
||
| 57 |
* a value in the near future. The deadline argument can be set to uint256(-1) to |
||
| 58 |
* create Permits that effectively never expire. |
||
| 59 |
*/ |
||
| 60 |
function nonces(address owner) external view returns (uint256); |
||
| 61 | |||
| 62 |
function version() external view returns (string memory); |
||
| 63 | |||
| 64 |
function permitTypeHash() external view returns (bytes32); |
||
| 65 | |||
| 66 |
function domainSeparator() external view returns (bytes32); |
||
| 67 |
} |
||
| 68 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: AGPL-3.0-only |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
import "./EnumerableSet.sol"; |
||
| 5 | |||
| 6 |
/// @notice Role based Authority that supports up to 256 roles. |
||
| 7 |
/// @author BadgerDAO |
||
| 8 |
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/authorities/RolesAuthority.sol) |
||
| 9 |
/// @author Modified from Dappsys (https://github.com/dapphub/ds-roles/blob/master/src/roles.sol) |
||
| 10 |
interface IRolesAuthority {
|
||
| 11 |
event UserRoleUpdated(address indexed user, uint8 indexed role, bool enabled); |
||
| 12 | |||
| 13 |
event PublicCapabilityUpdated(address indexed target, bytes4 indexed functionSig, bool enabled); |
||
| 14 |
event CapabilityBurned(address indexed target, bytes4 indexed functionSig); |
||
| 15 | |||
| 16 |
event RoleCapabilityUpdated( |
||
| 17 |
uint8 indexed role, |
||
| 18 |
address indexed target, |
||
| 19 |
bytes4 indexed functionSig, |
||
| 20 |
bool enabled |
||
| 21 |
); |
||
| 22 | |||
| 23 |
enum CapabilityFlag {
|
||
| 24 |
None, |
||
| 25 |
Public, |
||
| 26 |
Burned |
||
| 27 |
} |
||
| 28 |
} |
||
| 29 |
| Lines covered: | 4 / 11 (36.4%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol) |
||
| 3 | |||
| 4 |
pragma solidity 0.8.17; |
||
| 5 | |||
| 6 |
import "./Context.sol"; |
||
| 7 | |||
| 8 |
/** |
||
| 9 |
* @dev Contract module which provides a basic access control mechanism, where |
||
| 10 |
* there is an account (an owner) that can be granted exclusive access to |
||
| 11 |
* specific functions. |
||
| 12 |
* |
||
| 13 |
* By default, the owner account will be the one that deploys the contract. This |
||
| 14 |
* can later be changed with {transferOwnership}.
|
||
| 15 |
* |
||
| 16 |
* This module is used through inheritance. It will make available the modifier |
||
| 17 |
* `onlyOwner`, which can be applied to your functions to restrict their use to |
||
| 18 |
* the owner. |
||
| 19 |
*/ |
||
| 20 |
abstract contract Ownable is Context {
|
||
| 21 |
address private _owner; |
||
| 22 | |||
| 23 |
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); |
||
| 24 | |||
| 25 |
/** |
||
| 26 |
* @dev Initializes the contract setting the deployer as the initial owner. |
||
| 27 |
*/ |
||
| 28 |
constructor() {
|
||
| 29 |
√
|
_transferOwnership(_msgSender()); |
|
| 30 |
} |
||
| 31 | |||
| 32 |
/** |
||
| 33 |
* @dev Throws if called by any account other than the owner. |
||
| 34 |
*/ |
||
| 35 |
modifier onlyOwner() {
|
||
| 36 |
_checkOwner(); |
||
| 37 |
_; |
||
| 38 |
} |
||
| 39 | |||
| 40 |
/** |
||
| 41 |
* @dev Returns the address of the current owner. |
||
| 42 |
*/ |
||
| 43 |
function owner() public view virtual returns (address) {
|
||
| 44 |
return _owner; |
||
| 45 |
} |
||
| 46 | |||
| 47 |
/** |
||
| 48 |
* @dev Throws if the sender is not the owner. |
||
| 49 |
*/ |
||
| 50 |
function _checkOwner() internal view virtual {
|
||
| 51 |
require(owner() == _msgSender(), "Ownable: caller is not the owner"); |
||
| 52 |
} |
||
| 53 | |||
| 54 |
/** |
||
| 55 |
* @dev Leaves the contract without owner. It will not be possible to call |
||
| 56 |
* `onlyOwner` functions anymore. Can only be called by the current owner. |
||
| 57 |
* |
||
| 58 |
* NOTE: Renouncing ownership will leave the contract without an owner, |
||
| 59 |
* thereby removing any functionality that is only available to the owner. |
||
| 60 |
*/ |
||
| 61 |
function renounceOwnership() public virtual onlyOwner {
|
||
| 62 |
_transferOwnership(address(0)); |
||
| 63 |
} |
||
| 64 | |||
| 65 |
/** |
||
| 66 |
* @dev Transfers ownership of the contract to a new account (`newOwner`). |
||
| 67 |
* Can only be called by the current owner. |
||
| 68 |
*/ |
||
| 69 |
function transferOwnership(address newOwner) public virtual onlyOwner {
|
||
| 70 |
require(newOwner != address(0), "Ownable: new owner is the zero address"); |
||
| 71 |
_transferOwnership(newOwner); |
||
| 72 |
} |
||
| 73 | |||
| 74 |
/** |
||
| 75 |
* @dev Transfers ownership of the contract to a new account (`newOwner`). |
||
| 76 |
* Internal function without access restriction. |
||
| 77 |
*/ |
||
| 78 |
function _transferOwnership(address newOwner) internal virtual {
|
||
| 79 |
√
|
address oldOwner = _owner; |
|
| 80 |
√
|
_owner = newOwner; |
|
| 81 |
√
|
emit OwnershipTransferred(oldOwner, newOwner); |
|
| 82 |
} |
||
| 83 |
} |
||
| 84 |
| Lines covered: | 0 / 5 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 |
import "../Interfaces/IPermitNonce.sol"; |
||
| 5 | |||
| 6 |
/** |
||
| 7 |
* @dev This abstract contract provides a mapping from address to nonce (uint256) used for permit signature |
||
| 8 |
*/ |
||
| 9 |
contract PermitNonce is IPermitNonce {
|
||
| 10 |
mapping(address => uint256) internal _nonces; |
||
| 11 | |||
| 12 |
/// @dev Increase current nonce for msg.sender by one. |
||
| 13 |
/// @notice This function could be used to invalidate any signed permit out there |
||
| 14 |
function increasePermitNonce() external returns (uint256) {
|
||
| 15 |
return ++_nonces[msg.sender]; |
||
| 16 |
} |
||
| 17 | |||
| 18 |
/// @dev Return current nonce for msg.sender fOR EIP-2612 compatibility |
||
| 19 |
function nonces(address owner) external view virtual returns (uint256) {
|
||
| 20 |
return _nonces[owner]; |
||
| 21 |
} |
||
| 22 |
} |
||
| 23 |
| Lines covered: | 7 / 8 (87.5%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v4.6.0) (proxy/Proxy.sol) |
||
| 3 | |||
| 4 |
pragma solidity 0.8.17; |
||
| 5 | |||
| 6 |
/** |
||
| 7 |
* @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM |
||
| 8 |
* instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to |
||
| 9 |
* be specified by overriding the virtual {_implementation} function.
|
||
| 10 |
* |
||
| 11 |
* Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a
|
||
| 12 |
* different contract through the {_delegate} function.
|
||
| 13 |
* |
||
| 14 |
* The success and return data of the delegated call will be returned back to the caller of the proxy. |
||
| 15 |
* @dev BadgerDAO: Simplified to the core delegation functionality, without any additional features. |
||
| 16 |
*/ |
||
| 17 |
contract Proxy {
|
||
| 18 |
/** |
||
| 19 |
* @dev Delegates the current call to `implementation`. |
||
| 20 |
* |
||
| 21 |
* This function does not return to its internal call site, it will return directly to the external caller. |
||
| 22 |
*/ |
||
| 23 |
function _delegate(address implementation) internal virtual {
|
||
| 24 |
assembly {
|
||
| 25 |
// Copy msg.data. We take full control of memory in this inline assembly |
||
| 26 |
// block because it will not return to Solidity code. We overwrite the |
||
| 27 |
// Solidity scratch pad at memory position 0. |
||
| 28 |
√
|
⟳
|
calldatacopy(0, 0, calldatasize()) |
| 29 | |||
| 30 |
// Call the implementation. |
||
| 31 |
// out and outsize are 0 because we don't know the size yet. |
||
| 32 |
√
|
⟳
|
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) |
| 33 | |||
| 34 |
// Copy the returned data. |
||
| 35 |
√
|
⟳
|
returndatacopy(0, 0, returndatasize()) |
| 36 | |||
| 37 |
√
|
⟳
|
switch result |
| 38 |
// delegatecall returns 0 on error. |
||
| 39 |
√
|
⟳
|
case 0 {
|
| 40 |
⟳
|
revert(0, returndatasize()) |
|
| 41 |
} |
||
| 42 |
default {
|
||
| 43 |
√
|
return(0, returndatasize()) |
|
| 44 |
} |
||
| 45 |
} |
||
| 46 |
} |
||
| 47 |
} |
||
| 48 |
| Lines covered: | 3 / 6 (50.0%) |
|---|
| 1 |
// SPDX-License-Identifier: AGPL-3.0-only |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
/// @notice Gas optimized reentrancy protection for smart contracts. |
||
| 5 |
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/ReentrancyGuard.sol) |
||
| 6 |
/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) |
||
| 7 |
abstract contract ReentrancyGuard {
|
||
| 8 |
√
|
⟳
|
uint256 internal constant OPEN = 1; |
| 9 |
√
|
⟳
|
uint256 internal constant LOCKED = 2; |
| 10 | |||
| 11 |
√
|
⟳
|
uint256 public locked = OPEN; |
| 12 | |||
| 13 |
modifier nonReentrant() virtual {
|
||
| 14 |
require(locked == OPEN, "ReentrancyGuard: Reentrancy in nonReentrant call"); |
||
| 15 | |||
| 16 |
locked = LOCKED; |
||
| 17 | |||
| 18 |
_; |
||
| 19 | |||
| 20 |
locked = OPEN; |
||
| 21 |
} |
||
| 22 |
} |
||
| 23 |
| Lines covered: | 17 / 43 (39.5%) |
|---|
| 1 |
// SPDX-License-Identifier: AGPL-3.0-only |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
import {IRolesAuthority} from "./IRolesAuthority.sol";
|
||
| 5 |
import {Auth, Authority} from "./Auth.sol";
|
||
| 6 |
import "./EnumerableSet.sol"; |
||
| 7 | |||
| 8 |
/// @notice Role based Authority that supports up to 256 roles. |
||
| 9 |
/// @author BadgerDAO |
||
| 10 |
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/authorities/RolesAuthority.sol) |
||
| 11 |
/// @author Modified from Dappsys (https://github.com/dapphub/ds-roles/blob/master/src/roles.sol) |
||
| 12 |
contract RolesAuthority is IRolesAuthority, Auth, Authority {
|
||
| 13 |
using EnumerableSet for EnumerableSet.Bytes32Set; |
||
| 14 |
using EnumerableSet for EnumerableSet.AddressSet; |
||
| 15 | |||
| 16 |
/*////////////////////////////////////////////////////////////// |
||
| 17 |
CONSTRUCTOR |
||
| 18 |
//////////////////////////////////////////////////////////////*/ |
||
| 19 | |||
| 20 |
√
|
constructor(address _owner, Authority _authority) Auth(_owner, _authority) {}
|
|
| 21 | |||
| 22 |
/*////////////////////////////////////////////////////////////// |
||
| 23 |
ROLE/USER STORAGE |
||
| 24 |
//////////////////////////////////////////////////////////////*/ |
||
| 25 | |||
| 26 |
EnumerableSet.AddressSet internal users; |
||
| 27 |
EnumerableSet.AddressSet internal targets; |
||
| 28 |
mapping(address => EnumerableSet.Bytes32Set) internal enabledFunctionSigsByTarget; |
||
| 29 | |||
| 30 |
EnumerableSet.Bytes32Set internal enabledFunctionSigsPublic; |
||
| 31 | |||
| 32 |
mapping(address => bytes32) public getUserRoles; |
||
| 33 | |||
| 34 |
mapping(address => mapping(bytes4 => CapabilityFlag)) public capabilityFlag; |
||
| 35 | |||
| 36 |
mapping(address => mapping(bytes4 => bytes32)) public getRolesWithCapability; |
||
| 37 | |||
| 38 |
function doesUserHaveRole(address user, uint8 role) public view virtual returns (bool) {
|
||
| 39 |
return (uint256(getUserRoles[user]) >> role) & 1 != 0; |
||
| 40 |
} |
||
| 41 | |||
| 42 |
function doesRoleHaveCapability( |
||
| 43 |
uint8 role, |
||
| 44 |
address target, |
||
| 45 |
bytes4 functionSig |
||
| 46 |
) public view virtual returns (bool) {
|
||
| 47 |
return (uint256(getRolesWithCapability[target][functionSig]) >> role) & 1 != 0; |
||
| 48 |
} |
||
| 49 | |||
| 50 |
function isPublicCapability(address target, bytes4 functionSig) public view returns (bool) {
|
||
| 51 |
return capabilityFlag[target][functionSig] == CapabilityFlag.Public; |
||
| 52 |
} |
||
| 53 | |||
| 54 |
/*////////////////////////////////////////////////////////////// |
||
| 55 |
AUTHORIZATION LOGIC |
||
| 56 |
//////////////////////////////////////////////////////////////*/ |
||
| 57 | |||
| 58 |
/** |
||
| 59 |
@notice A user can call a given function signature on a given target address if: |
||
| 60 |
- The capability has not been burned |
||
| 61 |
- That capability is public, or the user has a role that has been granted the capability to call the function |
||
| 62 |
*/ |
||
| 63 |
function canCall( |
||
| 64 |
address user, |
||
| 65 |
address target, |
||
| 66 |
bytes4 functionSig |
||
| 67 |
√
|
⟳
|
) public view virtual override returns (bool) {
|
| 68 |
√
|
⟳
|
CapabilityFlag flag = capabilityFlag[target][functionSig]; |
| 69 | |||
| 70 |
√
|
⟳
|
if (flag == CapabilityFlag.Burned) {
|
| 71 |
return false; |
||
| 72 |
√
|
⟳
|
} else if (flag == CapabilityFlag.Public) {
|
| 73 |
return true; |
||
| 74 |
} else {
|
||
| 75 |
√
|
⟳
|
return bytes32(0) != getUserRoles[user] & getRolesWithCapability[target][functionSig]; |
| 76 |
} |
||
| 77 |
} |
||
| 78 | |||
| 79 |
/*////////////////////////////////////////////////////////////// |
||
| 80 |
ROLE CAPABILITY CONFIGURATION LOGIC |
||
| 81 |
//////////////////////////////////////////////////////////////*/ |
||
| 82 | |||
| 83 |
/// @notice Set a capability flag as public, meaning any account can call it. Or revoke this capability. |
||
| 84 |
/// @dev A capability cannot be made public if it has been burned. |
||
| 85 |
function setPublicCapability( |
||
| 86 |
address target, |
||
| 87 |
bytes4 functionSig, |
||
| 88 |
bool enabled |
||
| 89 |
) public virtual requiresAuth {
|
||
| 90 |
require( |
||
| 91 |
capabilityFlag[target][functionSig] != CapabilityFlag.Burned, |
||
| 92 |
"RolesAuthority: Capability Burned" |
||
| 93 |
); |
||
| 94 | |||
| 95 |
if (enabled) {
|
||
| 96 |
capabilityFlag[target][functionSig] = CapabilityFlag.Public; |
||
| 97 |
} else {
|
||
| 98 |
capabilityFlag[target][functionSig] = CapabilityFlag.None; |
||
| 99 |
} |
||
| 100 | |||
| 101 |
emit PublicCapabilityUpdated(target, functionSig, enabled); |
||
| 102 |
} |
||
| 103 | |||
| 104 |
/// @notice Grant a specified role the ability to call a function on a target. |
||
| 105 |
/// @notice Has no effect |
||
| 106 |
function setRoleCapability( |
||
| 107 |
uint8 role, |
||
| 108 |
address target, |
||
| 109 |
bytes4 functionSig, |
||
| 110 |
bool enabled |
||
| 111 |
) public virtual requiresAuth {
|
||
| 112 |
√
|
if (enabled) {
|
|
| 113 |
√
|
getRolesWithCapability[target][functionSig] |= bytes32(1 << role); |
|
| 114 |
√
|
enabledFunctionSigsByTarget[target].add(bytes32(functionSig)); |
|
| 115 | |||
| 116 |
√
|
if (!targets.contains(target)) {
|
|
| 117 |
√
|
targets.add(target); |
|
| 118 |
} |
||
| 119 |
} else {
|
||
| 120 |
getRolesWithCapability[target][functionSig] &= ~bytes32(1 << role); |
||
| 121 |
enabledFunctionSigsByTarget[target].remove(bytes32(functionSig)); |
||
| 122 | |||
| 123 |
// If no enabled function signatures exist for this target, remove target |
||
| 124 |
if (enabledFunctionSigsByTarget[target].length() == 0) {
|
||
| 125 |
targets.remove(target); |
||
| 126 |
} |
||
| 127 |
} |
||
| 128 | |||
| 129 |
√
|
emit RoleCapabilityUpdated(role, target, functionSig, enabled); |
|
| 130 |
} |
||
| 131 | |||
| 132 |
/// @notice Permanently burns a capability for a target. |
||
| 133 |
function burnCapability(address target, bytes4 functionSig) public virtual requiresAuth {
|
||
| 134 |
require( |
||
| 135 |
capabilityFlag[target][functionSig] != CapabilityFlag.Burned, |
||
| 136 |
"RolesAuthority: Capability Burned" |
||
| 137 |
); |
||
| 138 |
capabilityFlag[target][functionSig] = CapabilityFlag.Burned; |
||
| 139 | |||
| 140 |
emit CapabilityBurned(target, functionSig); |
||
| 141 |
} |
||
| 142 | |||
| 143 |
/*////////////////////////////////////////////////////////////// |
||
| 144 |
USER ROLE ASSIGNMENT LOGIC |
||
| 145 |
//////////////////////////////////////////////////////////////*/ |
||
| 146 | |||
| 147 |
function setUserRole(address user, uint8 role, bool enabled) public virtual requiresAuth {
|
||
| 148 |
√
|
if (enabled) {
|
|
| 149 |
√
|
getUserRoles[user] |= bytes32(1 << role); |
|
| 150 | |||
| 151 |
√
|
if (!users.contains(user)) {
|
|
| 152 |
√
|
users.add(user); |
|
| 153 |
} |
||
| 154 |
} else {
|
||
| 155 |
getUserRoles[user] &= ~bytes32(1 << role); |
||
| 156 | |||
| 157 |
// Remove user if no more roles |
||
| 158 |
if (getUserRoles[user] == bytes32(0)) {
|
||
| 159 |
users.remove(user); |
||
| 160 |
} |
||
| 161 |
} |
||
| 162 | |||
| 163 |
√
|
emit UserRoleUpdated(user, role, enabled); |
|
| 164 |
} |
||
| 165 |
} |
||
| 166 |
| Lines covered: | 0 / 5 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/utils/SafeERC20.sol) |
||
| 3 | |||
| 4 |
pragma solidity 0.8.17; |
||
| 5 | |||
| 6 |
import "./IERC20.sol"; |
||
| 7 |
import "./Address.sol"; |
||
| 8 | |||
| 9 |
/** |
||
| 10 |
* @title SafeERC20 |
||
| 11 |
* @dev Wrappers around ERC20 operations that throw on failure (when the token |
||
| 12 |
* contract returns false). Tokens that return no value (and instead revert or |
||
| 13 |
* throw on failure) are also supported, non-reverting calls are assumed to be |
||
| 14 |
* successful. |
||
| 15 |
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, |
||
| 16 |
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc. |
||
| 17 |
*/ |
||
| 18 |
library SafeERC20 {
|
||
| 19 |
using Address for address; |
||
| 20 | |||
| 21 |
/** |
||
| 22 |
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, |
||
| 23 |
* non-reverting calls are assumed to be successful. |
||
| 24 |
*/ |
||
| 25 |
function safeTransfer(IERC20 token, address to, uint256 value) internal {
|
||
| 26 |
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); |
||
| 27 |
} |
||
| 28 | |||
| 29 |
/// @dev Calls approve while checking bool return value, handles no-return tokens |
||
| 30 |
function safeApprove(IERC20 token, address spender, uint256 amount) internal {
|
||
| 31 |
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, amount)); |
||
| 32 |
} |
||
| 33 | |||
| 34 |
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
|
||
| 35 |
_callOptionalReturn( |
||
| 36 |
token, |
||
| 37 |
abi.encodeWithSelector(token.transferFrom.selector, from, to, value) |
||
| 38 |
); |
||
| 39 |
} |
||
| 40 | |||
| 41 |
/** |
||
| 42 |
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement |
||
| 43 |
* on the return value: the return value is optional (but if data is returned, it must not be false). |
||
| 44 |
* @param token The token targeted by the call. |
||
| 45 |
* @param data The call data (encoded using abi.encode or one of its variants). |
||
| 46 |
*/ |
||
| 47 |
function _callOptionalReturn(IERC20 token, bytes memory data) private {
|
||
| 48 |
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since |
||
| 49 |
// we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
|
||
| 50 |
// the target address contains contract code and also asserts for success in the low-level call. |
||
| 51 | |||
| 52 |
bytes memory returndata = address(token).functionCall( |
||
| 53 |
data, |
||
| 54 |
"SafeERC20: low-level call failed" |
||
| 55 |
); |
||
| 56 |
require( |
||
| 57 |
returndata.length == 0 || abi.decode(returndata, (bool)), |
||
| 58 |
"SafeERC20: ERC20 operation did not succeed" |
||
| 59 |
); |
||
| 60 |
} |
||
| 61 |
} |
||
| 62 |
| Lines covered: | 0 / 1 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
/** |
||
| 6 |
* Based on OpenZeppelin's SafeMath: |
||
| 7 |
* https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/SafeMath.sol |
||
| 8 |
* |
||
| 9 |
* @dev Wrappers over Solidity's arithmetic operations with added overflow |
||
| 10 |
* checks. |
||
| 11 |
* |
||
| 12 |
* Arithmetic operations in Solidity wrap on overflow. This can easily result |
||
| 13 |
* in bugs, because programmers usually assume that an overflow raises an |
||
| 14 |
* error, which is the standard behavior in high level programming languages. |
||
| 15 |
* `SafeMath` restores this intuition by reverting the transaction when an |
||
| 16 |
* operation overflows. |
||
| 17 |
* |
||
| 18 |
* Using this library instead of the unchecked operations eliminates an entire |
||
| 19 |
* class of bugs, so it's recommended to use it always. |
||
| 20 |
*/ |
||
| 21 |
library SafeMath {
|
||
| 22 |
/** |
||
| 23 |
* @dev Returns the addition of two unsigned integers, reverting on |
||
| 24 |
* overflow. |
||
| 25 |
* |
||
| 26 |
* Counterpart to Solidity's `+` operator. |
||
| 27 |
* |
||
| 28 |
* Requirements: |
||
| 29 |
* - Addition cannot overflow. |
||
| 30 |
*/ |
||
| 31 |
function add(uint256 a, uint256 b) internal pure returns (uint256) {
|
||
| 32 |
uint256 c = a + b; |
||
| 33 |
require(c >= a, "SafeMath: addition overflow"); |
||
| 34 | |||
| 35 |
return c; |
||
| 36 |
} |
||
| 37 | |||
| 38 |
/** |
||
| 39 |
* @dev Returns the subtraction of two unsigned integers, reverting on |
||
| 40 |
* overflow (when the result is negative). |
||
| 41 |
* |
||
| 42 |
* Counterpart to Solidity's `-` operator. |
||
| 43 |
* |
||
| 44 |
* Requirements: |
||
| 45 |
* - Subtraction cannot overflow. |
||
| 46 |
*/ |
||
| 47 |
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
|
||
| 48 |
return sub(a, b, "SafeMath: subtraction overflow"); |
||
| 49 |
} |
||
| 50 | |||
| 51 |
/** |
||
| 52 |
* @dev Returns the subtraction of two unsigned integers, reverting with custom message on |
||
| 53 |
* overflow (when the result is negative). |
||
| 54 |
* |
||
| 55 |
* Counterpart to Solidity's `-` operator. |
||
| 56 |
* |
||
| 57 |
* Requirements: |
||
| 58 |
* - Subtraction cannot overflow. |
||
| 59 |
* |
||
| 60 |
* _Available since v2.4.0._ |
||
| 61 |
*/ |
||
| 62 |
function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
|
||
| 63 |
require(b <= a, errorMessage); |
||
| 64 |
uint256 c = a - b; |
||
| 65 | |||
| 66 |
return c; |
||
| 67 |
} |
||
| 68 | |||
| 69 |
/** |
||
| 70 |
* @dev Returns the multiplication of two unsigned integers, reverting on |
||
| 71 |
* overflow. |
||
| 72 |
* |
||
| 73 |
* Counterpart to Solidity's `*` operator. |
||
| 74 |
* |
||
| 75 |
* Requirements: |
||
| 76 |
* - Multiplication cannot overflow. |
||
| 77 |
*/ |
||
| 78 |
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
|
||
| 79 |
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the |
||
| 80 |
// benefit is lost if 'b' is also tested. |
||
| 81 |
// See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 |
||
| 82 |
if (a == 0) {
|
||
| 83 |
return 0; |
||
| 84 |
} |
||
| 85 | |||
| 86 |
uint256 c = a * b; |
||
| 87 |
require(c / a == b, "SafeMath: multiplication overflow"); |
||
| 88 | |||
| 89 |
return c; |
||
| 90 |
} |
||
| 91 | |||
| 92 |
/** |
||
| 93 |
* @dev Returns the integer division of two unsigned integers. Reverts on |
||
| 94 |
* division by zero. The result is rounded towards zero. |
||
| 95 |
* |
||
| 96 |
* Counterpart to Solidity's `/` operator. Note: this function uses a |
||
| 97 |
* `revert` opcode (which leaves remaining gas untouched) while Solidity |
||
| 98 |
* uses an invalid opcode to revert (consuming all remaining gas). |
||
| 99 |
* |
||
| 100 |
* Requirements: |
||
| 101 |
* - The divisor cannot be zero. |
||
| 102 |
*/ |
||
| 103 |
function div(uint256 a, uint256 b) internal pure returns (uint256) {
|
||
| 104 |
return div(a, b, "SafeMath: division by zero"); |
||
| 105 |
} |
||
| 106 | |||
| 107 |
/** |
||
| 108 |
* @dev Returns the integer division of two unsigned integers. Reverts with custom message on |
||
| 109 |
* division by zero. The result is rounded towards zero. |
||
| 110 |
* |
||
| 111 |
* Counterpart to Solidity's `/` operator. Note: this function uses a |
||
| 112 |
* `revert` opcode (which leaves remaining gas untouched) while Solidity |
||
| 113 |
* uses an invalid opcode to revert (consuming all remaining gas). |
||
| 114 |
* |
||
| 115 |
* Requirements: |
||
| 116 |
* - The divisor cannot be zero. |
||
| 117 |
* |
||
| 118 |
* _Available since v2.4.0._ |
||
| 119 |
*/ |
||
| 120 |
function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
|
||
| 121 |
// Solidity only automatically asserts when dividing by 0 |
||
| 122 |
require(b > 0, errorMessage); |
||
| 123 |
uint256 c = a / b; |
||
| 124 |
// assert(a == b * c + a % b); // There is no case in which this doesn't hold |
||
| 125 | |||
| 126 |
return c; |
||
| 127 |
} |
||
| 128 | |||
| 129 |
/** |
||
| 130 |
* @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), |
||
| 131 |
* Reverts when dividing by zero. |
||
| 132 |
* |
||
| 133 |
* Counterpart to Solidity's `%` operator. This function uses a `revert` |
||
| 134 |
* opcode (which leaves remaining gas untouched) while Solidity uses an |
||
| 135 |
* invalid opcode to revert (consuming all remaining gas). |
||
| 136 |
* |
||
| 137 |
* Requirements: |
||
| 138 |
* - The divisor cannot be zero. |
||
| 139 |
*/ |
||
| 140 |
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
|
||
| 141 |
return mod(a, b, "SafeMath: modulo by zero"); |
||
| 142 |
} |
||
| 143 | |||
| 144 |
/** |
||
| 145 |
* @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), |
||
| 146 |
* Reverts with custom message when dividing by zero. |
||
| 147 |
* |
||
| 148 |
* Counterpart to Solidity's `%` operator. This function uses a `revert` |
||
| 149 |
* opcode (which leaves remaining gas untouched) while Solidity uses an |
||
| 150 |
* invalid opcode to revert (consuming all remaining gas). |
||
| 151 |
* |
||
| 152 |
* Requirements: |
||
| 153 |
* - The divisor cannot be zero. |
||
| 154 |
* |
||
| 155 |
* _Available since v2.4.0._ |
||
| 156 |
*/ |
||
| 157 |
function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
|
||
| 158 |
require(b != 0, errorMessage); |
||
| 159 |
return a % b; |
||
| 160 |
} |
||
| 161 |
} |
||
| 162 |
| Lines covered: | 31 / 43 (72.1%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Dependencies/Create3.sol"; |
||
| 6 |
import "./Dependencies/Ownable.sol"; |
||
| 7 | |||
| 8 |
√
|
contract EBTCDeployer is Ownable {
|
|
| 9 |
string public constant name = "eBTC Deployer"; |
||
| 10 | |||
| 11 |
√
|
string public constant AUTHORITY = "ebtc.v1.authority"; |
|
| 12 |
√
|
string public constant LIQUIDATION_LIBRARY = "ebtc.v1.liquidationLibrary"; |
|
| 13 |
√
|
string public constant CDP_MANAGER = "ebtc.v1.cdpManager"; |
|
| 14 |
√
|
string public constant BORROWER_OPERATIONS = "ebtc.v1.borrowerOperations"; |
|
| 15 | |||
| 16 |
√
|
string public constant PRICE_FEED = "ebtc.v1.priceFeed"; |
|
| 17 |
√
|
string public constant SORTED_CDPS = "ebtc.v1.sortedCdps"; |
|
| 18 | |||
| 19 |
√
|
string public constant ACTIVE_POOL = "ebtc.v1.activePool"; |
|
| 20 |
√
|
string public constant COLL_SURPLUS_POOL = "ebtc.v1.collSurplusPool"; |
|
| 21 | |||
| 22 |
√
|
string public constant HINT_HELPERS = "ebtc.v1.hintHelpers"; |
|
| 23 |
√
|
string public constant EBTC_TOKEN = "ebtc.v1.eBTCToken"; |
|
| 24 |
√
|
string public constant FEE_RECIPIENT = "ebtc.v1.feeRecipient"; |
|
| 25 |
string public constant MULTI_CDP_GETTER = "ebtc.v1.multiCdpGetter"; |
||
| 26 | |||
| 27 |
event ContractDeployed(address indexed contractAddress, string contractName, bytes32 salt); |
||
| 28 | |||
| 29 |
struct EbtcAddresses {
|
||
| 30 |
address authorityAddress; |
||
| 31 |
address liquidationLibraryAddress; |
||
| 32 |
address cdpManagerAddress; |
||
| 33 |
address borrowerOperationsAddress; |
||
| 34 |
address priceFeedAddress; |
||
| 35 |
address sortedCdpsAddress; |
||
| 36 |
address activePoolAddress; |
||
| 37 |
address collSurplusPoolAddress; |
||
| 38 |
address hintHelpersAddress; |
||
| 39 |
address ebtcTokenAddress; |
||
| 40 |
address feeRecipientAddress; |
||
| 41 |
address multiCdpGetterAddress; |
||
| 42 |
} |
||
| 43 | |||
| 44 |
/** |
||
| 45 |
@notice Helper method to return a set of future addresses for eBTC. Intended to be used in the order specified. |
||
| 46 |
|
||
| 47 |
@dev The order is as follows: |
||
| 48 |
0: authority |
||
| 49 |
1: liquidationLibrary |
||
| 50 |
2: cdpManager |
||
| 51 |
3: borrowerOperations |
||
| 52 |
4: priceFeed |
||
| 53 |
5; sortedCdps |
||
| 54 |
6: activePool |
||
| 55 |
7: collSurplusPool |
||
| 56 |
8: hintHelpers |
||
| 57 |
9: eBTCToken |
||
| 58 |
10: feeRecipient |
||
| 59 |
11: multiCdpGetter |
||
| 60 | |||
| 61 | |||
| 62 |
*/ |
||
| 63 |
√
|
function getFutureEbtcAddresses() public view returns (EbtcAddresses memory) {
|
|
| 64 |
√
|
EbtcAddresses memory addresses = EbtcAddresses( |
|
| 65 |
√
|
Create3.addressOf(keccak256(abi.encodePacked(AUTHORITY))), |
|
| 66 |
√
|
Create3.addressOf(keccak256(abi.encodePacked(LIQUIDATION_LIBRARY))), |
|
| 67 |
√
|
Create3.addressOf(keccak256(abi.encodePacked(CDP_MANAGER))), |
|
| 68 |
√
|
Create3.addressOf(keccak256(abi.encodePacked(BORROWER_OPERATIONS))), |
|
| 69 |
√
|
Create3.addressOf(keccak256(abi.encodePacked(PRICE_FEED))), |
|
| 70 |
√
|
Create3.addressOf(keccak256(abi.encodePacked(SORTED_CDPS))), |
|
| 71 |
√
|
Create3.addressOf(keccak256(abi.encodePacked(ACTIVE_POOL))), |
|
| 72 |
√
|
Create3.addressOf(keccak256(abi.encodePacked(COLL_SURPLUS_POOL))), |
|
| 73 |
√
|
Create3.addressOf(keccak256(abi.encodePacked(HINT_HELPERS))), |
|
| 74 |
√
|
Create3.addressOf(keccak256(abi.encodePacked(EBTC_TOKEN))), |
|
| 75 |
√
|
Create3.addressOf(keccak256(abi.encodePacked(FEE_RECIPIENT))), |
|
| 76 |
√
|
Create3.addressOf(keccak256(abi.encodePacked(MULTI_CDP_GETTER))) |
|
| 77 |
); |
||
| 78 | |||
| 79 |
√
|
return addresses; |
|
| 80 |
} |
||
| 81 | |||
| 82 |
/** |
||
| 83 |
@notice Deploy a contract using salt in string format and arbitrary runtime code. |
||
| 84 |
@dev Intended use is: get the future eBTC addresses, then deploy the appropriate contract to each address via this method, building the constructor using the mapped addresses |
||
| 85 |
@dev no enforcment of bytecode at address as we can't know the runtime code in this contract due to space constraints |
||
| 86 |
@dev gated to given deployer EOA to ensure no interference with process, given proper actions by deployer |
||
| 87 |
*/ |
||
| 88 |
function deploy( |
||
| 89 |
string memory _saltString, |
||
| 90 |
bytes memory _creationCode |
||
| 91 |
√
|
) public returns (address deployedAddress) {
|
|
| 92 |
√
|
bytes32 _salt = keccak256(abi.encodePacked(_saltString)); |
|
| 93 |
√
|
deployedAddress = Create3.create3(_salt, _creationCode); |
|
| 94 |
√
|
emit ContractDeployed(deployedAddress, _saltString, _salt); |
|
| 95 |
} |
||
| 96 | |||
| 97 |
function deployWithCreationCodeAndConstructorArgs( |
||
| 98 |
string memory _saltString, |
||
| 99 |
bytes memory creationCode, |
||
| 100 |
bytes memory constructionArgs |
||
| 101 |
) external returns (address) {
|
||
| 102 |
bytes memory _data = abi.encodePacked(creationCode, constructionArgs); |
||
| 103 |
return deploy(_saltString, _data); |
||
| 104 |
} |
||
| 105 | |||
| 106 |
function deployWithCreationCode( |
||
| 107 |
string memory _saltString, |
||
| 108 |
bytes memory creationCode |
||
| 109 |
) external returns (address) {
|
||
| 110 |
return deploy(_saltString, creationCode); |
||
| 111 |
} |
||
| 112 | |||
| 113 |
function addressOf(string memory _saltString) external view returns (address) {
|
||
| 114 |
bytes32 _salt = keccak256(abi.encodePacked(_saltString)); |
||
| 115 |
return Create3.addressOf(_salt); |
||
| 116 |
} |
||
| 117 | |||
| 118 |
function addressOfSalt(bytes32 _salt) external view returns (address) {
|
||
| 119 |
return Create3.addressOf(_salt); |
||
| 120 |
} |
||
| 121 | |||
| 122 |
/** |
||
| 123 |
@notice Create the creation code for a contract with the given runtime code. |
||
| 124 |
@dev credit: https://github.com/0xsequence/create3/blob/master/contracts/test_utils/Create3Imp.sol |
||
| 125 |
*/ |
||
| 126 |
function creationCodeFor(bytes memory _code) internal pure returns (bytes memory) {
|
||
| 127 |
/* |
||
| 128 |
0x00 0x63 0x63XXXXXX PUSH4 _code.length size |
||
| 129 |
0x01 0x80 0x80 DUP1 size size |
||
| 130 |
0x02 0x60 0x600e PUSH1 14 14 size size |
||
| 131 |
0x03 0x60 0x6000 PUSH1 00 0 14 size size |
||
| 132 |
0x04 0x39 0x39 CODECOPY size |
||
| 133 |
0x05 0x60 0x6000 PUSH1 00 0 size |
||
| 134 |
0x06 0xf3 0xf3 RETURN |
||
| 135 |
<CODE> |
||
| 136 |
*/ |
||
| 137 | |||
| 138 |
return |
||
| 139 |
abi.encodePacked(hex"63", uint32(_code.length), hex"80_60_0E_60_00_39_60_00_F3", _code); |
||
| 140 |
} |
||
| 141 |
} |
||
| 142 |
| Lines covered: | 41 / 104 (39.4%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Interfaces/IEBTCToken.sol"; |
||
| 6 | |||
| 7 |
import "./Dependencies/AuthNoOwner.sol"; |
||
| 8 |
import "./Dependencies/PermitNonce.sol"; |
||
| 9 | |||
| 10 |
/* |
||
| 11 |
* |
||
| 12 |
* Based upon OpenZeppelin's ERC20 contract: |
||
| 13 |
* https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol |
||
| 14 |
* |
||
| 15 |
* and their EIP2612 (ERC20Permit / ERC712) functionality: |
||
| 16 |
* https://github.com/OpenZeppelin/openzeppelin-contracts/blob/53516bc555a454862470e7860a9b5254db4d00f5/contracts/token/ERC20/ERC20Permit.sol |
||
| 17 |
* |
||
| 18 |
* |
||
| 19 |
* --- Functionality added specific to the EBTCToken --- |
||
| 20 |
* |
||
| 21 |
* 1) Transfer protection: blacklist of addresses that are invalid recipients (i.e. core Liquity contracts) in external |
||
| 22 |
* transfer() and transferFrom() calls. The purpose is to protect users from losing tokens by mistakenly sending EBTC directly to a Liquity |
||
| 23 |
* core contract, when they should rather call the right function. |
||
| 24 |
*/ |
||
| 25 | |||
| 26 |
contract EBTCToken is IEBTCToken, AuthNoOwner, PermitNonce {
|
||
| 27 |
uint256 private _totalSupply; |
||
| 28 |
string internal constant _NAME = "EBTC Stablecoin"; |
||
| 29 |
string internal constant _SYMBOL = "EBTC"; |
||
| 30 |
string internal constant _VERSION = "1"; |
||
| 31 |
uint8 internal constant _DECIMALS = 18; |
||
| 32 | |||
| 33 |
// --- Data for EIP2612 --- |
||
| 34 | |||
| 35 |
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
|
||
| 36 |
bytes32 private constant _PERMIT_TYPEHASH = |
||
| 37 |
0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; |
||
| 38 |
// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
|
||
| 39 |
bytes32 private constant _TYPE_HASH = |
||
| 40 |
√
|
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; |
|
| 41 | |||
| 42 |
// Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to |
||
| 43 |
// invalidate the cached domain separator if the chain id changes. |
||
| 44 |
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; |
||
| 45 |
uint256 private immutable _CACHED_CHAIN_ID; |
||
| 46 | |||
| 47 |
bytes32 private immutable _HASHED_NAME; |
||
| 48 |
bytes32 private immutable _HASHED_VERSION; |
||
| 49 | |||
| 50 |
// User data for EBTC token |
||
| 51 |
mapping(address => uint256) private _balances; |
||
| 52 |
mapping(address => mapping(address => uint256)) private _allowances; |
||
| 53 | |||
| 54 |
// --- Addresses --- |
||
| 55 |
address public immutable cdpManagerAddress; |
||
| 56 |
address public immutable borrowerOperationsAddress; |
||
| 57 | |||
| 58 |
constructor( |
||
| 59 |
address _cdpManagerAddress, |
||
| 60 |
address _borrowerOperationsAddress, |
||
| 61 |
address _authorityAddress |
||
| 62 |
) {
|
||
| 63 |
√
|
_initializeAuthority(_authorityAddress); |
|
| 64 | |||
| 65 |
√
|
cdpManagerAddress = _cdpManagerAddress; |
|
| 66 |
√
|
borrowerOperationsAddress = _borrowerOperationsAddress; |
|
| 67 | |||
| 68 |
√
|
bytes32 hashedName = keccak256(bytes(_NAME)); |
|
| 69 |
√
|
bytes32 hashedVersion = keccak256(bytes(_VERSION)); |
|
| 70 | |||
| 71 |
√
|
_HASHED_NAME = hashedName; |
|
| 72 |
√
|
_HASHED_VERSION = hashedVersion; |
|
| 73 |
√
|
_CACHED_CHAIN_ID = _chainID(); |
|
| 74 |
√
|
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(_TYPE_HASH, hashedName, hashedVersion); |
|
| 75 |
} |
||
| 76 | |||
| 77 |
// --- Functions for intra-Liquity calls --- |
||
| 78 | |||
| 79 |
/** |
||
| 80 |
* @notice Mint new tokens |
||
| 81 |
* @dev Internal system function - only callable by BorrowerOperations or CDPManager |
||
| 82 |
* @dev Governance can also expand the list of approved minters to enable other systems to mint tokens |
||
| 83 |
* @param _account The address to receive the newly minted tokens |
||
| 84 |
* @param _amount The amount of tokens to mint |
||
| 85 |
*/ |
||
| 86 |
function mint(address _account, uint256 _amount) external override {
|
||
| 87 |
√
|
_requireCallerIsBOorCdpMOrAuth(); |
|
| 88 |
√
|
_mint(_account, _amount); |
|
| 89 |
} |
||
| 90 | |||
| 91 |
/** |
||
| 92 |
* @notice Burn existing tokens |
||
| 93 |
* @dev Internal system function - only callable by BorrowerOperations or CDPManager |
||
| 94 |
* @dev Governance can also expand the list of approved burners to enable other systems to burn tokens |
||
| 95 |
* @param _account The address to burn tokens from |
||
| 96 |
* @param _amount The amount of tokens to burn |
||
| 97 |
*/ |
||
| 98 |
function burn(address _account, uint256 _amount) external override {
|
||
| 99 |
√
|
⟳
|
_requireCallerIsBOorCdpMOrAuth(); |
| 100 |
√
|
⟳
|
_burn(_account, _amount); |
| 101 |
} |
||
| 102 | |||
| 103 |
/** |
||
| 104 |
* @notice Burn existing tokens from caller |
||
| 105 |
* @dev Internal system function - only callable by BorrowerOperations or CDPManager |
||
| 106 |
* @dev Governance can also expand the list of approved burners to enable other systems to burn tokens |
||
| 107 |
* @param _amount The amount of tokens to burn |
||
| 108 |
*/ |
||
| 109 |
function burn(uint256 _amount) external {
|
||
| 110 |
_requireCallerIsBOorCdpMOrAuth(); |
||
| 111 |
_burn(msg.sender, _amount); |
||
| 112 |
} |
||
| 113 | |||
| 114 |
// --- External functions --- |
||
| 115 | |||
| 116 |
√
|
⟳
|
function totalSupply() external view override returns (uint256) {
|
| 117 |
√
|
⟳
|
return _totalSupply; |
| 118 |
} |
||
| 119 | |||
| 120 |
√
|
⟳
|
function balanceOf(address account) external view override returns (uint256) {
|
| 121 |
√
|
⟳
|
return _balances[account]; |
| 122 |
} |
||
| 123 | |||
| 124 |
function transfer(address recipient, uint256 amount) external override returns (bool) {
|
||
| 125 |
_requireValidRecipient(recipient); |
||
| 126 |
_transfer(msg.sender, recipient, amount); |
||
| 127 |
return true; |
||
| 128 |
} |
||
| 129 | |||
| 130 |
function allowance(address owner, address spender) external view override returns (uint256) {
|
||
| 131 |
return _allowances[owner][spender]; |
||
| 132 |
} |
||
| 133 | |||
| 134 |
√
|
function approve(address spender, uint256 amount) external override returns (bool) {
|
|
| 135 |
√
|
_approve(msg.sender, spender, amount); |
|
| 136 |
√
|
return true; |
|
| 137 |
} |
||
| 138 | |||
| 139 |
function transferFrom( |
||
| 140 |
address sender, |
||
| 141 |
address recipient, |
||
| 142 |
uint256 amount |
||
| 143 |
) external override returns (bool) {
|
||
| 144 |
_requireValidRecipient(recipient); |
||
| 145 |
_transfer(sender, recipient, amount); |
||
| 146 | |||
| 147 |
uint256 cachedAllowance = _allowances[sender][msg.sender]; |
||
| 148 |
if (cachedAllowance != type(uint256).max) {
|
||
| 149 |
require(cachedAllowance >= amount, "ERC20: transfer amount exceeds allowance"); |
||
| 150 |
unchecked {
|
||
| 151 |
_approve(sender, msg.sender, cachedAllowance - amount); |
||
| 152 |
} |
||
| 153 |
} |
||
| 154 |
return true; |
||
| 155 |
} |
||
| 156 | |||
| 157 |
function increaseAllowance( |
||
| 158 |
address spender, |
||
| 159 |
uint256 addedValue |
||
| 160 |
) external override returns (bool) {
|
||
| 161 |
_approve(msg.sender, spender, _allowances[msg.sender][spender] + addedValue); |
||
| 162 |
return true; |
||
| 163 |
} |
||
| 164 | |||
| 165 |
function decreaseAllowance( |
||
| 166 |
address spender, |
||
| 167 |
uint256 subtractedValue |
||
| 168 |
) external override returns (bool) {
|
||
| 169 |
uint256 cachedAllowances = _allowances[msg.sender][spender]; |
||
| 170 |
require(cachedAllowances >= subtractedValue, "ERC20: decreased allowance below zero"); |
||
| 171 |
unchecked {
|
||
| 172 |
_approve(msg.sender, spender, cachedAllowances - subtractedValue); |
||
| 173 |
} |
||
| 174 |
return true; |
||
| 175 |
} |
||
| 176 | |||
| 177 |
// --- EIP 2612 Functionality (https://eips.ethereum.org/EIPS/eip-2612) --- |
||
| 178 | |||
| 179 |
function DOMAIN_SEPARATOR() external view returns (bytes32) {
|
||
| 180 |
return domainSeparator(); |
||
| 181 |
} |
||
| 182 | |||
| 183 |
function domainSeparator() public view override returns (bytes32) {
|
||
| 184 |
if (_chainID() == _CACHED_CHAIN_ID) {
|
||
| 185 |
return _CACHED_DOMAIN_SEPARATOR; |
||
| 186 |
} else {
|
||
| 187 |
return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION); |
||
| 188 |
} |
||
| 189 |
} |
||
| 190 | |||
| 191 |
function permit( |
||
| 192 |
address owner, |
||
| 193 |
address spender, |
||
| 194 |
uint256 amount, |
||
| 195 |
uint256 deadline, |
||
| 196 |
uint8 v, |
||
| 197 |
bytes32 r, |
||
| 198 |
bytes32 s |
||
| 199 |
) external override {
|
||
| 200 |
require(deadline >= block.timestamp, "EBTC: expired deadline"); |
||
| 201 |
bytes32 digest = keccak256( |
||
| 202 |
abi.encodePacked( |
||
| 203 |
"\x19\x01", |
||
| 204 |
domainSeparator(), |
||
| 205 |
keccak256( |
||
| 206 |
abi.encode(_PERMIT_TYPEHASH, owner, spender, amount, _nonces[owner]++, deadline) |
||
| 207 |
) |
||
| 208 |
) |
||
| 209 |
); |
||
| 210 |
address recoveredAddress = ecrecover(digest, v, r, s); |
||
| 211 |
require(recoveredAddress == owner, "EBTC: invalid signature"); |
||
| 212 |
_approve(owner, spender, amount); |
||
| 213 |
} |
||
| 214 | |||
| 215 |
/// @dev Return current nonce for msg.sender fOR EIP-2612 compatibility |
||
| 216 |
function nonces(address owner) external view override(IERC2612, PermitNonce) returns (uint256) {
|
||
| 217 |
return _nonces[owner]; |
||
| 218 |
} |
||
| 219 | |||
| 220 |
// --- Internal operations --- |
||
| 221 | |||
| 222 |
√
|
function _chainID() private view returns (uint256) {
|
|
| 223 |
√
|
return block.chainid; |
|
| 224 |
} |
||
| 225 | |||
| 226 |
function _buildDomainSeparator( |
||
| 227 |
bytes32 typeHash, |
||
| 228 |
bytes32 name, |
||
| 229 |
bytes32 version |
||
| 230 |
√
|
) private view returns (bytes32) {
|
|
| 231 |
√
|
return keccak256(abi.encode(typeHash, name, version, _chainID(), address(this))); |
|
| 232 |
} |
||
| 233 | |||
| 234 |
// --- Internal operations --- |
||
| 235 |
// Warning: sanity checks (for sender and recipient) should have been done before calling these internal functions |
||
| 236 | |||
| 237 |
function _transfer(address sender, address recipient, uint256 amount) internal {
|
||
| 238 |
require(sender != address(0), "EBTCToken: zero sender!"); |
||
| 239 |
require(recipient != address(0), "EBTCToken: zero recipient!"); |
||
| 240 | |||
| 241 |
uint256 cachedSenderBalances = _balances[sender]; |
||
| 242 |
require(cachedSenderBalances >= amount, "ERC20: transfer amount exceeds balance"); |
||
| 243 | |||
| 244 |
unchecked {
|
||
| 245 |
// Safe because of the check above |
||
| 246 |
_balances[sender] = cachedSenderBalances - amount; |
||
| 247 |
} |
||
| 248 | |||
| 249 |
_balances[recipient] = _balances[recipient] + amount; |
||
| 250 |
emit Transfer(sender, recipient, amount); |
||
| 251 |
} |
||
| 252 | |||
| 253 |
function _mint(address account, uint256 amount) internal {
|
||
| 254 |
√
|
require(account != address(0), "EBTCToken: mint to zero recipient!"); |
|
| 255 | |||
| 256 |
√
|
_totalSupply = _totalSupply + amount; |
|
| 257 |
√
|
_balances[account] = _balances[account] + amount; |
|
| 258 |
√
|
emit Transfer(address(0), account, amount); |
|
| 259 |
} |
||
| 260 | |||
| 261 |
function _burn(address account, uint256 amount) internal {
|
||
| 262 |
√
|
⟳
|
require(account != address(0), "EBTCToken: burn from zero account!"); |
| 263 | |||
| 264 |
√
|
⟳
|
uint256 cachedBalance = _balances[account]; |
| 265 |
√
|
⟳
|
require(cachedBalance >= amount, "ERC20: burn amount exceeds balance"); |
| 266 | |||
| 267 |
unchecked {
|
||
| 268 |
// Safe because of the check above |
||
| 269 |
√
|
⟳
|
_balances[account] = cachedBalance - amount; |
| 270 |
} |
||
| 271 | |||
| 272 |
√
|
⟳
|
_totalSupply = _totalSupply - amount; |
| 273 |
√
|
⟳
|
emit Transfer(account, address(0), amount); |
| 274 |
} |
||
| 275 | |||
| 276 |
function _approve(address owner, address spender, uint256 amount) internal {
|
||
| 277 |
√
|
require(owner != address(0), "EBTCToken: zero approve owner!"); |
|
| 278 |
√
|
require(spender != address(0), "EBTCToken: zero approve spender!"); |
|
| 279 | |||
| 280 |
√
|
_allowances[owner][spender] = amount; |
|
| 281 |
√
|
emit Approval(owner, spender, amount); |
|
| 282 |
} |
||
| 283 | |||
| 284 |
// --- 'require' functions --- |
||
| 285 | |||
| 286 |
function _requireValidRecipient(address _recipient) internal view {
|
||
| 287 |
require( |
||
| 288 |
_recipient != address(0) && _recipient != address(this), |
||
| 289 |
"EBTC: Cannot transfer tokens directly to the EBTC token contract or the zero address" |
||
| 290 |
); |
||
| 291 |
require( |
||
| 292 |
_recipient != cdpManagerAddress && _recipient != borrowerOperationsAddress, |
||
| 293 |
"EBTC: Cannot transfer tokens directly to the CdpManager or BorrowerOps" |
||
| 294 |
); |
||
| 295 |
} |
||
| 296 | |||
| 297 |
function _requireCallerIsBorrowerOperations() internal view {
|
||
| 298 |
require( |
||
| 299 |
msg.sender == borrowerOperationsAddress, |
||
| 300 |
"EBTCToken: Caller is not BorrowerOperations" |
||
| 301 |
); |
||
| 302 |
} |
||
| 303 | |||
| 304 |
/// @dev authority check last to short-circuit in the case of use by usual immutable addresses |
||
| 305 |
function _requireCallerIsBOorCdpMOrAuth() internal view {
|
||
| 306 |
require( |
||
| 307 |
√
|
⟳
|
msg.sender == borrowerOperationsAddress || |
| 308 |
√
|
⟳
|
msg.sender == cdpManagerAddress || |
| 309 |
isAuthorized(msg.sender, msg.sig), |
||
| 310 |
"EBTC: Caller is neither BorrowerOperations nor CdpManager nor authorized" |
||
| 311 |
); |
||
| 312 |
} |
||
| 313 | |||
| 314 |
function _requireCallerIsCdpM() internal view {
|
||
| 315 |
require(msg.sender == cdpManagerAddress, "EBTC: Caller is not CdpManager"); |
||
| 316 |
} |
||
| 317 | |||
| 318 |
// --- Optional functions --- |
||
| 319 | |||
| 320 |
function name() external pure override returns (string memory) {
|
||
| 321 |
return _NAME; |
||
| 322 |
} |
||
| 323 | |||
| 324 |
function symbol() external pure override returns (string memory) {
|
||
| 325 |
return _SYMBOL; |
||
| 326 |
} |
||
| 327 | |||
| 328 |
function decimals() external pure override returns (uint8) {
|
||
| 329 |
return _DECIMALS; |
||
| 330 |
} |
||
| 331 | |||
| 332 |
function version() external pure override returns (string memory) {
|
||
| 333 |
return _VERSION; |
||
| 334 |
} |
||
| 335 | |||
| 336 |
function permitTypeHash() external pure override returns (bytes32) {
|
||
| 337 |
return _PERMIT_TYPEHASH; |
||
| 338 |
} |
||
| 339 |
} |
||
| 340 |
| Lines covered: | 2 / 8 (25.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Dependencies/Ownable.sol"; |
||
| 6 |
import "./Dependencies/AuthNoOwner.sol"; |
||
| 7 |
import "./Dependencies/IERC20.sol"; |
||
| 8 |
import "./Dependencies/SafeERC20.sol"; |
||
| 9 | |||
| 10 |
/** |
||
| 11 |
@notice Minimal fee recipient |
||
| 12 |
@notice Tokens can be swept to owner address by authorized user |
||
| 13 |
*/ |
||
| 14 |
contract FeeRecipient is Ownable, AuthNoOwner {
|
||
| 15 |
using SafeERC20 for IERC20; |
||
| 16 |
// --- Events --- |
||
| 17 |
event SweepTokenSuccess(address indexed _token, uint256 _amount, address indexed _recipient); |
||
| 18 | |||
| 19 |
// --- Data --- |
||
| 20 |
string public constant NAME = "FeeRecipient"; |
||
| 21 | |||
| 22 |
constructor(address _ownerAddress, address _authorityAddress) {
|
||
| 23 |
√
|
_transferOwnership(_ownerAddress); |
|
| 24 |
√
|
_initializeAuthority(_authorityAddress); |
|
| 25 |
} |
||
| 26 | |||
| 27 |
// === Governed Functions === // |
||
| 28 | |||
| 29 |
/// @dev Function to move unintended dust that are not protected |
||
| 30 |
/// @notice moves given amount of given token (collateral is NOT allowed) |
||
| 31 |
/// @notice because recipient are fixed, this function is safe to be called by anyone |
||
| 32 |
function sweepToken(address token, uint256 amount) public requiresAuth {
|
||
| 33 |
uint256 balance = IERC20(token).balanceOf(address(this)); |
||
| 34 |
require(amount <= balance, "FeeRecipient: Attempt to sweep more than balance"); |
||
| 35 | |||
| 36 |
address _owner = owner(); |
||
| 37 |
IERC20(token).safeTransfer(_owner, amount); |
||
| 38 | |||
| 39 |
emit SweepTokenSuccess(token, amount, _owner); |
||
| 40 |
} |
||
| 41 |
} |
||
| 42 |
| Lines covered: | 4 / 66 (6.1%) |
|---|
| 1 |
// SPDX-License-Identifier: AGPL-3.0-only |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
import {EnumerableSet} from "./Dependencies/EnumerableSet.sol";
|
||
| 5 |
import {Authority} from "./Dependencies/Auth.sol";
|
||
| 6 |
import {RolesAuthority} from "./Dependencies/RolesAuthority.sol";
|
||
| 7 | |||
| 8 |
/// @notice Role based Authority that supports up to 256 roles. |
||
| 9 |
/// @notice We have taken the tradeoff of additional storage usage for easier readabiliy without using off-chain / indexing services. |
||
| 10 |
/// @author BadgerDAO Expanded from Solmate RolesAuthority |
||
| 11 |
/// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/auth/authorities/RolesAuthority.sol) |
||
| 12 |
/// @author Modified from Dappsys (https://github.com/dapphub/ds-roles/blob/master/src/roles.sol) |
||
| 13 |
contract Governor is RolesAuthority {
|
||
| 14 |
using EnumerableSet for EnumerableSet.Bytes32Set; |
||
| 15 |
using EnumerableSet for EnumerableSet.AddressSet; |
||
| 16 | |||
| 17 |
√
|
bytes32 NO_ROLES = bytes32(0); |
|
| 18 | |||
| 19 |
struct Role {
|
||
| 20 |
uint8 roleId; |
||
| 21 |
string roleName; |
||
| 22 |
} |
||
| 23 | |||
| 24 |
struct Capability {
|
||
| 25 |
address target; |
||
| 26 |
bytes4 functionSig; |
||
| 27 |
uint8[] roles; |
||
| 28 |
} |
||
| 29 | |||
| 30 |
/*////////////////////////////////////////////////////////////// |
||
| 31 |
STORAGE |
||
| 32 |
//////////////////////////////////////////////////////////////*/ |
||
| 33 | |||
| 34 |
mapping(uint8 => string) internal roleNames; |
||
| 35 | |||
| 36 |
/*////////////////////////////////////////////////////////////// |
||
| 37 |
EVENTS |
||
| 38 |
//////////////////////////////////////////////////////////////*/ |
||
| 39 | |||
| 40 |
event RoleNameSet(uint8 indexed role, string indexed name); |
||
| 41 | |||
| 42 |
/*////////////////////////////////////////////////////////////// |
||
| 43 |
CONSTRUCTOR |
||
| 44 |
//////////////////////////////////////////////////////////////*/ |
||
| 45 | |||
| 46 |
/// @notice The contract constructor initializes RolesAuthority with the given owner. |
||
| 47 |
/// @param _owner The address of the owner, who gains all permissions by default. |
||
| 48 |
√
|
constructor(address _owner) RolesAuthority(_owner, Authority(address(this))) {}
|
|
| 49 | |||
| 50 |
/*////////////////////////////////////////////////////////////// |
||
| 51 |
GETTERS |
||
| 52 |
//////////////////////////////////////////////////////////////*/ |
||
| 53 | |||
| 54 |
/// @notice Returns a list of users that are assigned a specific role. |
||
| 55 |
/// @dev This function searches all users and checks if they are assigned the given role. |
||
| 56 |
/// @dev Intended for off-chain utility only due to inefficiency. |
||
| 57 |
/// @param role The role ID to find users for. |
||
| 58 |
/// @return usersWithRole An array of addresses that are assigned the given role. |
||
| 59 | |||
| 60 |
function getUsersByRole(uint8 role) external view returns (address[] memory usersWithRole) {
|
||
| 61 |
// Search over all users: O(n) * 2 |
||
| 62 |
uint256 count; |
||
| 63 |
for (uint256 i = 0; i < users.length(); i++) {
|
||
| 64 |
address user = users.at(i); |
||
| 65 |
bool _canCall = doesUserHaveRole(user, role); |
||
| 66 |
if (_canCall) {
|
||
| 67 |
count += 1; |
||
| 68 |
} |
||
| 69 |
} |
||
| 70 |
if (count > 0) {
|
||
| 71 |
uint256 j = 0; |
||
| 72 |
usersWithRole = new address[](count); |
||
| 73 |
address[] memory _usrs = users.values(); |
||
| 74 |
for (uint256 i = 0; i < _usrs.length; i++) {
|
||
| 75 |
address user = _usrs[i]; |
||
| 76 |
bool _canCall = doesUserHaveRole(user, role); |
||
| 77 |
if (_canCall) {
|
||
| 78 |
usersWithRole[j] = user; |
||
| 79 |
j++; |
||
| 80 |
} |
||
| 81 |
} |
||
| 82 |
} |
||
| 83 |
} |
||
| 84 | |||
| 85 |
/// @notice Returns a list of roles that an address has. |
||
| 86 |
/// @dev This function searches all roles and checks if they are assigned to the given user. |
||
| 87 |
/// @dev Intended for off-chain utility only due to inefficiency. |
||
| 88 |
/// @param user The address of the user. |
||
| 89 |
/// @return rolesForUser An array of role IDs that the user has. |
||
| 90 | |||
| 91 |
function getRolesForUser(address user) external view returns (uint8[] memory rolesForUser) {
|
||
| 92 |
// Enumerate over all possible roles and check if enabled |
||
| 93 |
uint256 count; |
||
| 94 |
for (uint8 i = 0; i < type(uint8).max; i++) {
|
||
| 95 |
if (doesUserHaveRole(user, i)) {
|
||
| 96 |
count += 1; |
||
| 97 |
} |
||
| 98 |
} |
||
| 99 |
if (count > 0) {
|
||
| 100 |
uint256 j = 0; |
||
| 101 |
rolesForUser = new uint8[](count); |
||
| 102 |
for (uint8 i = 0; i < type(uint8).max; i++) {
|
||
| 103 |
if (doesUserHaveRole(user, i)) {
|
||
| 104 |
rolesForUser[j] = i; |
||
| 105 |
j++; |
||
| 106 |
} |
||
| 107 |
} |
||
| 108 |
} |
||
| 109 |
} |
||
| 110 | |||
| 111 |
function getRolesFromByteMap(bytes32 byteMap) public pure returns (uint8[] memory roleIds) {
|
||
| 112 |
uint256 count; |
||
| 113 |
for (uint8 i = 0; i < type(uint8).max; i++) {
|
||
| 114 |
bool roleEnabled = (uint256(byteMap >> i) & 1) != 0; |
||
| 115 |
if (roleEnabled) {
|
||
| 116 |
count += 1; |
||
| 117 |
} |
||
| 118 |
} |
||
| 119 |
if (count > 0) {
|
||
| 120 |
uint256 j = 0; |
||
| 121 |
roleIds = new uint8[](count); |
||
| 122 |
for (uint8 i = 0; i < type(uint8).max; i++) {
|
||
| 123 |
bool roleEnabled = (uint256(byteMap >> i) & 1) != 0; |
||
| 124 |
if (roleEnabled) {
|
||
| 125 |
roleIds[j] = i; |
||
| 126 |
j++; |
||
| 127 |
} |
||
| 128 |
} |
||
| 129 |
} |
||
| 130 |
} |
||
| 131 | |||
| 132 |
// helper function to generate bytes32 cache data for given roleIds array |
||
| 133 |
function getByteMapFromRoles(uint8[] memory roleIds) public pure returns (bytes32) {
|
||
| 134 |
bytes32 _data; |
||
| 135 |
for (uint8 i = 0; i < roleIds.length; i++) {
|
||
| 136 |
_data |= bytes32(1 << uint256(roleIds[i])); |
||
| 137 |
} |
||
| 138 |
return _data; |
||
| 139 |
} |
||
| 140 | |||
| 141 |
// helper function to return every authorization-enabled function signatures for given target address |
||
| 142 |
function getEnabledFunctionsInTarget( |
||
| 143 |
address _target |
||
| 144 |
) public view returns (bytes4[] memory _funcs) {
|
||
| 145 |
bytes32[] memory _sigs = enabledFunctionSigsByTarget[_target].values(); |
||
| 146 |
if (_sigs.length > 0) {
|
||
| 147 |
_funcs = new bytes4[](_sigs.length); |
||
| 148 |
for (uint256 i = 0; i < _sigs.length; ++i) {
|
||
| 149 |
_funcs[i] = bytes4(_sigs[i]); |
||
| 150 |
} |
||
| 151 |
} |
||
| 152 |
} |
||
| 153 | |||
| 154 |
/// @notice return all role IDs that have at least one capability enabled |
||
| 155 |
function getActiveRoles() external view returns (Role[] memory activeRoles) {
|
||
| 156 |
revert("Planned off-chain QOL function, not yet implemented, please ignore for audit");
|
||
| 157 |
} |
||
| 158 | |||
| 159 |
// If a role exists, flip enabled |
||
| 160 | |||
| 161 |
// Return all roles that are enabled anywhere |
||
| 162 | |||
| 163 |
function getCapabilitiesForTarget( |
||
| 164 |
address target |
||
| 165 |
) external view returns (Capability[] memory capabilities) {
|
||
| 166 |
revert("Planned off-chain QOL function, not yet implemented, please ignore for audit");
|
||
| 167 |
} |
||
| 168 | |||
| 169 |
function getCapabilitiesByRole( |
||
| 170 |
uint8 role |
||
| 171 |
) external view returns (Capability[] memory capabilities) {
|
||
| 172 |
revert("Planned off-chain QOL function, not yet implemented, please ignore for audit");
|
||
| 173 |
} |
||
| 174 | |||
| 175 |
function getRoleName(uint8 role) external view returns (string memory roleName) {
|
||
| 176 |
return roleNames[role]; |
||
| 177 |
} |
||
| 178 | |||
| 179 |
/*////////////////////////////////////////////////////////////// |
||
| 180 |
AUTHORIZED SETTERS |
||
| 181 |
//////////////////////////////////////////////////////////////*/ |
||
| 182 | |||
| 183 |
function setRoleName(uint8 role, string memory roleName) external requiresAuth {
|
||
| 184 |
// TODO: require maximum size for a name |
||
| 185 |
√
|
roleNames[role] = roleName; |
|
| 186 | |||
| 187 |
√
|
emit RoleNameSet(role, roleName); |
|
| 188 |
} |
||
| 189 |
} |
||
| 190 |
| Lines covered: | 3 / 91 (3.3%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Interfaces/ICdpManager.sol"; |
||
| 6 |
import "./Interfaces/ISortedCdps.sol"; |
||
| 7 |
import "./Dependencies/EbtcBase.sol"; |
||
| 8 | |||
| 9 |
contract HintHelpers is EbtcBase {
|
||
| 10 |
string public constant NAME = "HintHelpers"; |
||
| 11 | |||
| 12 |
ISortedCdps public immutable sortedCdps; |
||
| 13 |
ICdpManager public immutable cdpManager; |
||
| 14 | |||
| 15 |
// --- Events --- |
||
| 16 | |||
| 17 |
struct LocalVariables_getRedemptionHints {
|
||
| 18 |
uint256 remainingEbtcToRedeem; |
||
| 19 |
uint256 minNetDebtInBTC; |
||
| 20 |
bytes32 currentCdpId; |
||
| 21 |
address currentCdpUser; |
||
| 22 |
} |
||
| 23 | |||
| 24 |
// --- Dependency setters --- |
||
| 25 |
constructor( |
||
| 26 |
address _sortedCdpsAddress, |
||
| 27 |
address _cdpManagerAddress, |
||
| 28 |
address _collateralAddress, |
||
| 29 |
address _activePoolAddress, |
||
| 30 |
address _priceFeedAddress |
||
| 31 |
√
|
) EbtcBase(_activePoolAddress, _priceFeedAddress, _collateralAddress) {
|
|
| 32 |
√
|
sortedCdps = ISortedCdps(_sortedCdpsAddress); |
|
| 33 |
√
|
cdpManager = ICdpManager(_cdpManagerAddress); |
|
| 34 |
} |
||
| 35 | |||
| 36 |
// --- Functions --- |
||
| 37 | |||
| 38 |
/** |
||
| 39 |
* @notice Get the redemption hints for the specified amount of eBTC, price and maximum number of iterations. |
||
| 40 |
* @param _EBTCamount The amount of eBTC to be redeemed. |
||
| 41 |
* @param _price The current price of the asset. |
||
| 42 |
* @param _maxIterations The maximum number of iterations to be performed. |
||
| 43 |
* @return firstRedemptionHint The identifier of the first CDP to be considered for redemption. |
||
| 44 |
* @return partialRedemptionHintNICR The new Nominal Collateral Ratio (NICR) of the CDP after partial redemption. |
||
| 45 |
* @return truncatedEBTCamount The actual amount of eBTC that can be redeemed. |
||
| 46 |
* @return partialRedemptionNewColl The new collateral amount after partial redemption. |
||
| 47 |
*/ |
||
| 48 |
function getRedemptionHints( |
||
| 49 |
uint256 _EBTCamount, |
||
| 50 |
uint256 _price, |
||
| 51 |
uint256 _maxIterations |
||
| 52 |
) |
||
| 53 |
external |
||
| 54 |
view |
||
| 55 |
returns ( |
||
| 56 |
bytes32 firstRedemptionHint, |
||
| 57 |
uint256 partialRedemptionHintNICR, |
||
| 58 |
uint256 truncatedEBTCamount, |
||
| 59 |
uint256 partialRedemptionNewColl |
||
| 60 |
) |
||
| 61 |
{
|
||
| 62 |
LocalVariables_getRedemptionHints memory vars; |
||
| 63 |
{
|
||
| 64 |
vars.remainingEbtcToRedeem = _EBTCamount; |
||
| 65 |
vars.currentCdpId = sortedCdps.getLast(); |
||
| 66 |
vars.currentCdpUser = sortedCdps.getOwnerAddress(vars.currentCdpId); |
||
| 67 | |||
| 68 |
while ( |
||
| 69 |
vars.currentCdpUser != address(0) && |
||
| 70 |
cdpManager.getICR(vars.currentCdpId, _price) < MCR |
||
| 71 |
) {
|
||
| 72 |
vars.currentCdpId = sortedCdps.getPrev(vars.currentCdpId); |
||
| 73 |
vars.currentCdpUser = sortedCdps.getOwnerAddress(vars.currentCdpId); |
||
| 74 |
} |
||
| 75 |
firstRedemptionHint = vars.currentCdpId; |
||
| 76 |
} |
||
| 77 | |||
| 78 |
if (_maxIterations == 0) {
|
||
| 79 |
_maxIterations = type(uint256).max; |
||
| 80 |
} |
||
| 81 | |||
| 82 |
// Underflow is intentionally used in _maxIterations-- > 0 |
||
| 83 |
unchecked {
|
||
| 84 |
while ( |
||
| 85 |
vars.currentCdpUser != address(0) && |
||
| 86 |
vars.remainingEbtcToRedeem > 0 && |
||
| 87 |
_maxIterations-- > 0 |
||
| 88 |
) {
|
||
| 89 |
// Apply pending debt |
||
| 90 |
uint256 currentCdpDebt = cdpManager.getCdpDebt(vars.currentCdpId) + |
||
| 91 |
cdpManager.getPendingRedistributedDebt(vars.currentCdpId); |
||
| 92 | |||
| 93 |
// If this CDP has more debt than the remaining to redeem, attempt a partial redemption |
||
| 94 |
if (currentCdpDebt > vars.remainingEbtcToRedeem) {
|
||
| 95 |
uint256 _cachedEbtcToRedeem = vars.remainingEbtcToRedeem; |
||
| 96 |
( |
||
| 97 |
partialRedemptionNewColl, |
||
| 98 |
partialRedemptionHintNICR |
||
| 99 |
) = _calculateCdpStateAfterPartialRedemption(vars, currentCdpDebt, _price); |
||
| 100 | |||
| 101 |
// If the partial redemption would leave the CDP with less than the minimum allowed coll, bail out of partial redemption and return only the fully redeemable |
||
| 102 |
// TODO: This seems to return the original coll? why? |
||
| 103 |
if (collateral.getPooledEthByShares(partialRedemptionNewColl) < MIN_NET_COLL) {
|
||
| 104 |
partialRedemptionHintNICR = 0; //reset to 0 as there is no partial redemption in this case |
||
| 105 |
partialRedemptionNewColl = 0; |
||
| 106 |
vars.remainingEbtcToRedeem = _cachedEbtcToRedeem; |
||
| 107 |
} else {
|
||
| 108 |
vars.remainingEbtcToRedeem = 0; |
||
| 109 |
} |
||
| 110 |
break; |
||
| 111 |
} else {
|
||
| 112 |
vars.remainingEbtcToRedeem = vars.remainingEbtcToRedeem - currentCdpDebt; |
||
| 113 |
} |
||
| 114 | |||
| 115 |
vars.currentCdpId = sortedCdps.getPrev(vars.currentCdpId); |
||
| 116 |
vars.currentCdpUser = sortedCdps.getOwnerAddress(vars.currentCdpId); |
||
| 117 |
} |
||
| 118 |
} |
||
| 119 | |||
| 120 |
truncatedEBTCamount = _EBTCamount - vars.remainingEbtcToRedeem; |
||
| 121 |
} |
||
| 122 | |||
| 123 |
/** |
||
| 124 |
* @notice Calculate the partial redemption information. |
||
| 125 |
* @dev This is an internal function used by getRedemptionHints. |
||
| 126 |
* @param vars The local variables of the getRedemptionHints function. |
||
| 127 |
* @param currentCdpDebt The net eBTC debt of the CDP. |
||
| 128 |
* @param _price The current price of the asset. |
||
| 129 |
* @return newCollShare The new collateral share amount after partial redemption. |
||
| 130 |
* @return newNICR The new Nominal Collateral Ratio (NICR) of the CDP after partial redemption. |
||
| 131 |
*/ |
||
| 132 |
function _calculateCdpStateAfterPartialRedemption( |
||
| 133 |
LocalVariables_getRedemptionHints memory vars, |
||
| 134 |
uint256 currentCdpDebt, |
||
| 135 |
uint256 _price |
||
| 136 |
) internal view returns (uint256, uint256) {
|
||
| 137 |
// maxReemable = min(remainingToRedeem, currentDebt) |
||
| 138 |
uint256 maxRedeemableEBTC = EbtcMath._min(vars.remainingEbtcToRedeem, currentCdpDebt); |
||
| 139 | |||
| 140 |
uint256 newCollShare; |
||
| 141 |
uint256 _oldIndex = cdpManager.stEthIndex(); |
||
| 142 |
uint256 _newIndex = collateral.getPooledEthByShares(DECIMAL_PRECISION); |
||
| 143 | |||
| 144 |
if (_oldIndex < _newIndex) {
|
||
| 145 |
newCollShare = _getCollateralWithSplitFeeApplied( |
||
| 146 |
vars.currentCdpId, |
||
| 147 |
_newIndex, |
||
| 148 |
_oldIndex |
||
| 149 |
); |
||
| 150 |
} else {
|
||
| 151 |
(, newCollShare, ) = cdpManager.getDebtAndCollShares(vars.currentCdpId); |
||
| 152 |
} |
||
| 153 | |||
| 154 |
vars.remainingEbtcToRedeem = vars.remainingEbtcToRedeem - maxRedeemableEBTC; |
||
| 155 |
uint256 collShareToReceive = collateral.getSharesByPooledEth( |
||
| 156 |
(maxRedeemableEBTC * DECIMAL_PRECISION) / _price |
||
| 157 |
); |
||
| 158 | |||
| 159 |
uint256 _newCollShareAfter = newCollShare - collShareToReceive; |
||
| 160 |
return ( |
||
| 161 |
_newCollShareAfter, |
||
| 162 |
EbtcMath._computeNominalCR(_newCollShareAfter, currentCdpDebt - maxRedeemableEBTC) |
||
| 163 |
); |
||
| 164 |
} |
||
| 165 | |||
| 166 |
/** |
||
| 167 |
* @notice Get the collateral amount of a CDP after applying split fee. |
||
| 168 |
* @dev This is an internal function used by _calculateCdpStateAfterPartialRedemption. |
||
| 169 |
* @param _cdpId The identifier of the CDP. |
||
| 170 |
* @param _newIndex The new index after the split fee is applied. |
||
| 171 |
* @param _oldIndex The old index before the split fee is applied. |
||
| 172 |
* @return newCollShare The new collateral share amount of the CDP after applying split fee. |
||
| 173 |
*/ |
||
| 174 |
function _getCollateralWithSplitFeeApplied( |
||
| 175 |
bytes32 _cdpId, |
||
| 176 |
uint256 _newIndex, |
||
| 177 |
uint256 _oldIndex |
||
| 178 |
) internal view returns (uint256) {
|
||
| 179 |
uint256 _deltaFeePerUnit; |
||
| 180 |
uint256 _newStFeePerUnit; |
||
| 181 |
uint256 _perUnitError; |
||
| 182 |
uint256 _feeTaken; |
||
| 183 | |||
| 184 |
(_feeTaken, _deltaFeePerUnit, _perUnitError) = cdpManager.calcFeeUponStakingReward( |
||
| 185 |
_newIndex, |
||
| 186 |
_oldIndex |
||
| 187 |
); |
||
| 188 |
_newStFeePerUnit = _deltaFeePerUnit + cdpManager.systemStEthFeePerUnitIndex(); |
||
| 189 |
(, uint256 newCollShare) = cdpManager.getAccumulatedFeeSplitApplied( |
||
| 190 |
_cdpId, |
||
| 191 |
_newStFeePerUnit |
||
| 192 |
); |
||
| 193 |
return newCollShare; |
||
| 194 |
} |
||
| 195 | |||
| 196 |
/* getApproxHint() - return address of a Cdp that is, on average, (length / numTrials) positions away in the |
||
| 197 |
sortedCdps list from the correct insert position of the Cdp to be inserted. |
||
| 198 |
|
||
| 199 |
Note: The output address is worst-case O(n) positions away from the correct insert position, however, the function |
||
| 200 |
is probabilistic. Input can be tuned to guarantee results to a high degree of confidence, e.g: |
||
| 201 | |||
| 202 |
Submitting numTrials = k * sqrt(length), with k = 15 makes it very, very likely that the ouput address will |
||
| 203 |
be <= sqrt(length) positions away from the correct insert position. |
||
| 204 |
*/ |
||
| 205 |
function getApproxHint( |
||
| 206 |
uint256 _CR, |
||
| 207 |
uint256 _numTrials, |
||
| 208 |
uint256 _inputRandomSeed |
||
| 209 |
) external view returns (bytes32 hint, uint256 diff, uint256 latestRandomSeed) {
|
||
| 210 |
uint256 arrayLength = cdpManager.getActiveCdpsCount(); |
||
| 211 | |||
| 212 |
if (arrayLength == 0) {
|
||
| 213 |
return (sortedCdps.nonExistId(), 0, _inputRandomSeed); |
||
| 214 |
} |
||
| 215 | |||
| 216 |
hint = sortedCdps.getLast(); |
||
| 217 |
diff = EbtcMath._getAbsoluteDifference(_CR, cdpManager.getNominalICR(hint)); |
||
| 218 |
latestRandomSeed = _inputRandomSeed; |
||
| 219 | |||
| 220 |
uint256 i = 1; |
||
| 221 | |||
| 222 |
while (i < _numTrials) {
|
||
| 223 |
latestRandomSeed = uint256(keccak256(abi.encodePacked(latestRandomSeed))); |
||
| 224 | |||
| 225 |
uint256 arrayIndex = latestRandomSeed % arrayLength; |
||
| 226 |
bytes32 _cId = cdpManager.getIdFromCdpIdsArray(arrayIndex); |
||
| 227 |
uint256 currentNICR = cdpManager.getNominalICR(_cId); |
||
| 228 | |||
| 229 |
// check if abs(current - CR) > abs(closest - CR), and update closest if current is closer |
||
| 230 |
uint256 currentDiff = EbtcMath._getAbsoluteDifference(currentNICR, _CR); |
||
| 231 | |||
| 232 |
if (currentDiff < diff) {
|
||
| 233 |
diff = currentDiff; |
||
| 234 |
hint = _cId; |
||
| 235 |
} |
||
| 236 |
i++; |
||
| 237 |
} |
||
| 238 |
} |
||
| 239 | |||
| 240 |
/// @notice Compute nominal CR for a specified collateral and debt amount |
||
| 241 |
function computeNominalCR(uint256 _coll, uint256 _debt) external pure returns (uint256) {
|
||
| 242 |
return EbtcMath._computeNominalCR(_coll, _debt); |
||
| 243 |
} |
||
| 244 | |||
| 245 |
/// @notice Compute CR for a specified collateral and debt amount |
||
| 246 |
function computeCR( |
||
| 247 |
uint256 _coll, |
||
| 248 |
uint256 _debt, |
||
| 249 |
uint256 _price |
||
| 250 |
) external pure returns (uint256) {
|
||
| 251 |
return EbtcMath._computeCR(_coll, _debt, _price); |
||
| 252 |
} |
||
| 253 |
} |
||
| 254 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./IPool.sol"; |
||
| 6 | |||
| 7 |
interface IActivePool is IPool {
|
||
| 8 |
// --- Events --- |
||
| 9 |
event ActivePoolEBTCDebtUpdated(uint256 _EBTCDebt); |
||
| 10 |
event SystemCollSharesUpdated(uint256 _coll); |
||
| 11 |
event FeeRecipientAddressChanged(address _feeRecipientAddress); |
||
| 12 |
event FeeRecipientClaimableCollSharesIncreased(uint256 _coll, uint256 _fee); |
||
| 13 |
event FeeRecipientClaimableCollSharesDecreased(uint256 _coll, uint256 _fee); |
||
| 14 |
event FlashLoanSuccess(address _receiver, address _token, uint256 _amount, uint256 _fee); |
||
| 15 |
event SweepTokenSuccess(address indexed _token, uint256 _amount, address indexed _recipient); |
||
| 16 | |||
| 17 |
// --- Functions --- |
||
| 18 |
function transferSystemCollShares(address _account, uint256 _amount) external; |
||
| 19 | |||
| 20 |
function increaseSystemCollShares(uint256 _value) external; |
||
| 21 | |||
| 22 |
function transferSystemCollSharesAndLiquidatorReward( |
||
| 23 |
address _account, |
||
| 24 |
uint256 _shares, |
||
| 25 |
uint256 _liquidatorRewardShares |
||
| 26 |
) external; |
||
| 27 | |||
| 28 |
function allocateSystemCollSharesToFeeRecipient(uint256 _shares) external; |
||
| 29 | |||
| 30 |
function claimFeeRecipientCollShares(uint256 _shares) external; |
||
| 31 | |||
| 32 |
function feeRecipientAddress() external view returns (address); |
||
| 33 | |||
| 34 |
function getFeeRecipientClaimableCollShares() external view returns (uint256); |
||
| 35 | |||
| 36 |
function setFeeRecipientAddress(address _feeRecipientAddress) external; |
||
| 37 |
} |
||
| 38 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 |
import "./IPositionManagers.sol"; |
||
| 5 | |||
| 6 |
// Common interface for the Cdp Manager. |
||
| 7 |
interface IBorrowerOperations is IPositionManagers {
|
||
| 8 |
// --- Events --- |
||
| 9 | |||
| 10 |
event FeeRecipientAddressChanged(address _feeRecipientAddress); |
||
| 11 |
event FlashLoanSuccess(address _receiver, address _token, uint256 _amount, uint256 _fee); |
||
| 12 | |||
| 13 |
// --- Functions --- |
||
| 14 | |||
| 15 |
function openCdp( |
||
| 16 |
uint256 _EBTCAmount, |
||
| 17 |
bytes32 _upperHint, |
||
| 18 |
bytes32 _lowerHint, |
||
| 19 |
uint256 _stEthBalance |
||
| 20 |
) external returns (bytes32); |
||
| 21 | |||
| 22 |
function openCdpFor( |
||
| 23 |
uint _EBTCAmount, |
||
| 24 |
bytes32 _upperHint, |
||
| 25 |
bytes32 _lowerHint, |
||
| 26 |
uint _collAmount, |
||
| 27 |
address _borrower |
||
| 28 |
) external returns (bytes32); |
||
| 29 | |||
| 30 |
function addColl( |
||
| 31 |
bytes32 _cdpId, |
||
| 32 |
bytes32 _upperHint, |
||
| 33 |
bytes32 _lowerHint, |
||
| 34 |
uint256 _stEthBalanceIncrease |
||
| 35 |
) external; |
||
| 36 | |||
| 37 |
function withdrawColl( |
||
| 38 |
bytes32 _cdpId, |
||
| 39 |
uint256 _stEthBalanceDecrease, |
||
| 40 |
bytes32 _upperHint, |
||
| 41 |
bytes32 _lowerHint |
||
| 42 |
) external; |
||
| 43 | |||
| 44 |
function withdrawDebt( |
||
| 45 |
bytes32 _cdpId, |
||
| 46 |
uint256 _amount, |
||
| 47 |
bytes32 _upperHint, |
||
| 48 |
bytes32 _lowerHint |
||
| 49 |
) external; |
||
| 50 | |||
| 51 |
function repayDebt( |
||
| 52 |
bytes32 _cdpId, |
||
| 53 |
uint256 _amount, |
||
| 54 |
bytes32 _upperHint, |
||
| 55 |
bytes32 _lowerHint |
||
| 56 |
) external; |
||
| 57 | |||
| 58 |
function closeCdp(bytes32 _cdpId) external; |
||
| 59 | |||
| 60 |
function adjustCdp( |
||
| 61 |
bytes32 _cdpId, |
||
| 62 |
uint256 _stEthBalanceDecrease, |
||
| 63 |
uint256 _debtChange, |
||
| 64 |
bool isDebtIncrease, |
||
| 65 |
bytes32 _upperHint, |
||
| 66 |
bytes32 _lowerHint |
||
| 67 |
) external; |
||
| 68 | |||
| 69 |
function adjustCdpWithColl( |
||
| 70 |
bytes32 _cdpId, |
||
| 71 |
uint256 _stEthBalanceDecrease, |
||
| 72 |
uint256 _debtChange, |
||
| 73 |
bool isDebtIncrease, |
||
| 74 |
bytes32 _upperHint, |
||
| 75 |
bytes32 _lowerHint, |
||
| 76 |
uint256 _stEthBalanceIncrease |
||
| 77 |
) external; |
||
| 78 | |||
| 79 |
function claimSurplusCollShares() external; |
||
| 80 | |||
| 81 |
function feeRecipientAddress() external view returns (address); |
||
| 82 |
} |
||
| 83 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./IEbtcBase.sol"; |
||
| 6 |
import "./ICdpManagerData.sol"; |
||
| 7 | |||
| 8 |
// Common interface for the Cdp Manager. |
||
| 9 |
interface ICdpManager is IEbtcBase, ICdpManagerData {
|
||
| 10 |
// --- Functions --- |
||
| 11 |
function getActiveCdpsCount() external view returns (uint256); |
||
| 12 | |||
| 13 |
function getIdFromCdpIdsArray(uint256 _index) external view returns (bytes32); |
||
| 14 | |||
| 15 |
function liquidate(bytes32 _cdpId) external; |
||
| 16 | |||
| 17 |
function partiallyLiquidate( |
||
| 18 |
bytes32 _cdpId, |
||
| 19 |
uint256 _partialAmount, |
||
| 20 |
bytes32 _upperPartialHint, |
||
| 21 |
bytes32 _lowerPartialHint |
||
| 22 |
) external; |
||
| 23 | |||
| 24 |
function batchLiquidateCdps(bytes32[] calldata _cdpArray) external; |
||
| 25 | |||
| 26 |
function redeemCollateral( |
||
| 27 |
uint256 _EBTCAmount, |
||
| 28 |
bytes32 _firstRedemptionHint, |
||
| 29 |
bytes32 _upperPartialRedemptionHint, |
||
| 30 |
bytes32 _lowerPartialRedemptionHint, |
||
| 31 |
uint256 _partialRedemptionHintNICR, |
||
| 32 |
uint256 _maxIterations, |
||
| 33 |
uint256 _maxFee |
||
| 34 |
) external; |
||
| 35 | |||
| 36 |
function updateStakeAndTotalStakes(bytes32 _cdpId) external returns (uint256); |
||
| 37 | |||
| 38 |
function syncAccounting(bytes32 _cdpId) external; |
||
| 39 | |||
| 40 |
function getTotalStakeForFeeTaken(uint256 _feeTaken) external view returns (uint256, uint256); |
||
| 41 | |||
| 42 |
function closeCdp(bytes32 _cdpId, address _borrower, uint256 _debt, uint256 _coll) external; |
||
| 43 | |||
| 44 |
function getRedemptionRate() external view returns (uint256); |
||
| 45 | |||
| 46 |
function getRedemptionRateWithDecay() external view returns (uint256); |
||
| 47 | |||
| 48 |
function getRedemptionFeeWithDecay(uint256 _ETHDrawn) external view returns (uint256); |
||
| 49 | |||
| 50 |
function decayBaseRateFromBorrowing() external; |
||
| 51 | |||
| 52 |
function getCdpStatus(bytes32 _cdpId) external view returns (uint256); |
||
| 53 | |||
| 54 |
function getCdpStake(bytes32 _cdpId) external view returns (uint256); |
||
| 55 | |||
| 56 |
function getCdpDebt(bytes32 _cdpId) external view returns (uint256); |
||
| 57 | |||
| 58 |
function getCdpCollShares(bytes32 _cdpId) external view returns (uint256); |
||
| 59 | |||
| 60 |
function getCdpLiquidatorRewardShares(bytes32 _cdpId) external view returns (uint); |
||
| 61 | |||
| 62 |
function initializeCdp( |
||
| 63 |
bytes32 _cdpId, |
||
| 64 |
uint256 _debt, |
||
| 65 |
uint256 _coll, |
||
| 66 |
uint256 _liquidatorRewardShares, |
||
| 67 |
address _borrower |
||
| 68 |
) external; |
||
| 69 | |||
| 70 |
function updateCdp( |
||
| 71 |
bytes32 _cdpId, |
||
| 72 |
address _borrower, |
||
| 73 |
uint256 _coll, |
||
| 74 |
uint256 _debt, |
||
| 75 |
uint256 _newColl, |
||
| 76 |
uint256 _newDebt |
||
| 77 |
) external; |
||
| 78 | |||
| 79 |
function getTCR(uint256 _price) external view returns (uint256); |
||
| 80 | |||
| 81 |
function checkRecoveryMode(uint256 _price) external view returns (bool); |
||
| 82 |
} |
||
| 83 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./ICollSurplusPool.sol"; |
||
| 6 |
import "./IEBTCToken.sol"; |
||
| 7 |
import "./ISortedCdps.sol"; |
||
| 8 |
import "./IActivePool.sol"; |
||
| 9 |
import "./IRecoveryModeGracePeriod.sol"; |
||
| 10 |
import "../Dependencies/ICollateralTokenOracle.sol"; |
||
| 11 | |||
| 12 |
// Common interface for the Cdp Manager. |
||
| 13 |
interface ICdpManagerData is IRecoveryModeGracePeriod {
|
||
| 14 |
// --- Events --- |
||
| 15 | |||
| 16 |
event FeeRecipientAddressChanged(address _feeRecipientAddress); |
||
| 17 |
event StakingRewardSplitSet(uint256 _stakingRewardSplit); |
||
| 18 |
event RedemptionFeeFloorSet(uint256 _redemptionFeeFloor); |
||
| 19 |
event MinuteDecayFactorSet(uint256 _minuteDecayFactor); |
||
| 20 |
event BetaSet(uint256 _beta); |
||
| 21 |
event RedemptionsPaused(bool _paused); |
||
| 22 | |||
| 23 |
event Liquidation(uint256 _liquidatedDebt, uint256 _liquidatedColl, uint256 _liqReward); |
||
| 24 |
event Redemption( |
||
| 25 |
uint256 _debtToRedeemExpected, |
||
| 26 |
uint256 _debtToRedeemActual, |
||
| 27 |
uint256 _collSharesSent, |
||
| 28 |
uint256 _feeCollShares, |
||
| 29 |
address _redeemer |
||
| 30 |
); |
||
| 31 |
event CdpUpdated( |
||
| 32 |
bytes32 indexed _cdpId, |
||
| 33 |
address indexed _borrower, |
||
| 34 |
uint256 _oldDebt, |
||
| 35 |
uint256 _oldCollShares, |
||
| 36 |
uint256 _debt, |
||
| 37 |
uint256 _collShares, |
||
| 38 |
uint256 _stake, |
||
| 39 |
CdpOperation _operation |
||
| 40 |
); |
||
| 41 |
event CdpLiquidated( |
||
| 42 |
bytes32 indexed _cdpId, |
||
| 43 |
address indexed _borrower, |
||
| 44 |
uint _debt, |
||
| 45 |
uint _collShares, |
||
| 46 |
CdpOperation _operation, |
||
| 47 |
address _liquidator, |
||
| 48 |
uint _premiumToLiquidator |
||
| 49 |
); |
||
| 50 |
event CdpPartiallyLiquidated( |
||
| 51 |
bytes32 indexed _cdpId, |
||
| 52 |
address indexed _borrower, |
||
| 53 |
uint256 _debt, |
||
| 54 |
uint256 _collShares, |
||
| 55 |
CdpOperation operation, |
||
| 56 |
address _liquidator, |
||
| 57 |
uint _premiumToLiquidator |
||
| 58 |
); |
||
| 59 |
event BaseRateUpdated(uint256 _baseRate); |
||
| 60 |
event LastRedemptionTimestampUpdated(uint256 _lastFeeOpTime); |
||
| 61 |
event TotalStakesUpdated(uint256 _newTotalStakes); |
||
| 62 |
event SystemSnapshotsUpdated(uint256 _totalStakesSnapshot, uint256 _totalCollateralSnapshot); |
||
| 63 |
event SystemDebtRedistributionIndexUpdated(uint256 _systemDebtRedistributionIndex); |
||
| 64 |
event CdpDebtRedistributionIndexUpdated(bytes32 _cdpId, uint256 _cdpDebtRedistributionIndex); |
||
| 65 |
event CdpArrayIndexUpdated(bytes32 _cdpId, uint256 _newIndex); |
||
| 66 |
event StEthIndexUpdated(uint256 _oldIndex, uint256 _newIndex, uint256 _updTimestamp); |
||
| 67 |
event CollateralFeePerUnitUpdated(uint256 _oldPerUnit, uint256 _newPerUnit, uint256 _feeTaken); |
||
| 68 |
event CdpFeeSplitApplied( |
||
| 69 |
bytes32 _cdpId, |
||
| 70 |
uint256 _oldPerUnitCdp, |
||
| 71 |
uint256 _newPerUnitCdp, |
||
| 72 |
uint256 _collReduced, |
||
| 73 |
uint256 collLeft |
||
| 74 |
); |
||
| 75 | |||
| 76 |
enum CdpOperation {
|
||
| 77 |
openCdp, |
||
| 78 |
closeCdp, |
||
| 79 |
adjustCdp, |
||
| 80 |
syncAccounting, |
||
| 81 |
liquidateInNormalMode, |
||
| 82 |
liquidateInRecoveryMode, |
||
| 83 |
redeemCollateral, |
||
| 84 |
partiallyLiquidate |
||
| 85 |
} |
||
| 86 | |||
| 87 |
enum Status {
|
||
| 88 |
nonExistent, |
||
| 89 |
active, |
||
| 90 |
closedByOwner, |
||
| 91 |
closedByLiquidation, |
||
| 92 |
closedByRedemption |
||
| 93 |
} |
||
| 94 | |||
| 95 |
// Store the necessary data for a cdp |
||
| 96 |
struct Cdp {
|
||
| 97 |
uint256 debt; |
||
| 98 |
uint256 coll; |
||
| 99 |
uint256 stake; |
||
| 100 |
uint256 liquidatorRewardShares; |
||
| 101 |
Status status; |
||
| 102 |
uint128 arrayIndex; |
||
| 103 |
} |
||
| 104 | |||
| 105 |
/* |
||
| 106 |
* --- Variable container structs for liquidations --- |
||
| 107 |
* |
||
| 108 |
* These structs are used to hold, return and assign variables inside the liquidation functions, |
||
| 109 |
* in order to avoid the error: "CompilerError: Stack too deep". |
||
| 110 |
**/ |
||
| 111 | |||
| 112 |
struct CdpDebtAndCollShares {
|
||
| 113 |
uint256 entireDebt; |
||
| 114 |
uint256 entireColl; |
||
| 115 |
uint256 pendingDebtReward; |
||
| 116 |
} |
||
| 117 | |||
| 118 |
struct LiquidationLocals {
|
||
| 119 |
bytes32 cdpId; |
||
| 120 |
uint256 partialAmount; // used only for partial liquidation, default 0 means full liquidation |
||
| 121 |
uint256 price; |
||
| 122 |
uint256 ICR; |
||
| 123 |
bytes32 upperPartialHint; |
||
| 124 |
bytes32 lowerPartialHint; |
||
| 125 |
bool recoveryModeAtStart; |
||
| 126 |
uint256 TCR; |
||
| 127 |
uint256 totalSurplusCollShares; |
||
| 128 |
uint256 totalCollSharesToSend; |
||
| 129 |
uint256 totalDebtToBurn; |
||
| 130 |
uint256 totalDebtToRedistribute; |
||
| 131 |
uint256 totalLiquidatorRewardCollShares; |
||
| 132 |
} |
||
| 133 | |||
| 134 |
struct LiquidationRecoveryModeLocals {
|
||
| 135 |
uint256 entireSystemDebt; |
||
| 136 |
uint256 entireSystemColl; |
||
| 137 |
uint256 totalDebtToBurn; |
||
| 138 |
uint256 totalCollSharesToSend; |
||
| 139 |
uint256 totalSurplusCollShares; |
||
| 140 |
bytes32 cdpId; |
||
| 141 |
uint256 price; |
||
| 142 |
uint256 ICR; |
||
| 143 |
uint256 totalDebtToRedistribute; |
||
| 144 |
uint256 totalLiquidatorRewardCollShares; |
||
| 145 |
} |
||
| 146 | |||
| 147 |
struct LocalVariables_OuterLiquidationFunction {
|
||
| 148 |
uint256 price; |
||
| 149 |
bool recoveryModeAtStart; |
||
| 150 |
uint256 liquidatedDebt; |
||
| 151 |
uint256 liquidatedColl; |
||
| 152 |
} |
||
| 153 | |||
| 154 |
struct LocalVariables_LiquidationSequence {
|
||
| 155 |
uint256 i; |
||
| 156 |
uint256 ICR; |
||
| 157 |
bytes32 cdpId; |
||
| 158 |
bool backToNormalMode; |
||
| 159 |
uint256 entireSystemDebt; |
||
| 160 |
uint256 entireSystemColl; |
||
| 161 |
uint256 price; |
||
| 162 |
uint256 TCR; |
||
| 163 |
} |
||
| 164 | |||
| 165 |
struct SingleRedemptionInputs {
|
||
| 166 |
bytes32 cdpId; |
||
| 167 |
uint256 maxEBTCamount; |
||
| 168 |
uint256 price; |
||
| 169 |
bytes32 upperPartialRedemptionHint; |
||
| 170 |
bytes32 lowerPartialRedemptionHint; |
||
| 171 |
uint256 partialRedemptionHintNICR; |
||
| 172 |
} |
||
| 173 | |||
| 174 |
struct LiquidationValues {
|
||
| 175 |
uint256 entireCdpDebt; |
||
| 176 |
uint256 debtToBurn; |
||
| 177 |
uint256 totalCollToSendToLiquidator; |
||
| 178 |
uint256 debtToRedistribute; |
||
| 179 |
uint256 collSurplus; |
||
| 180 |
uint256 liquidatorCollSharesReward; |
||
| 181 |
} |
||
| 182 | |||
| 183 |
struct LiquidationTotals {
|
||
| 184 |
uint256 totalDebtInSequence; |
||
| 185 |
uint256 totalDebtToBurn; |
||
| 186 |
uint256 totalCollToSendToLiquidator; |
||
| 187 |
uint256 totalDebtToRedistribute; |
||
| 188 |
uint256 totalCollSurplus; |
||
| 189 |
uint256 totalCollReward; |
||
| 190 |
} |
||
| 191 | |||
| 192 |
// --- Variable container structs for redemptions --- |
||
| 193 | |||
| 194 |
struct RedemptionTotals {
|
||
| 195 |
uint256 remainingDebtToRedeem; |
||
| 196 |
uint256 debtToRedeem; |
||
| 197 |
uint256 collSharesDrawn; |
||
| 198 |
uint256 totalCollSharesSurplus; |
||
| 199 |
uint256 feeCollShares; |
||
| 200 |
uint256 collSharesToRedeemer; |
||
| 201 |
uint256 decayedBaseRate; |
||
| 202 |
uint256 price; |
||
| 203 |
uint256 systemDebtAtStart; |
||
| 204 |
uint256 systemCollSharesAtStart; |
||
| 205 |
uint256 tcrAtStart; |
||
| 206 |
} |
||
| 207 | |||
| 208 |
struct SingleRedemptionValues {
|
||
| 209 |
uint256 debtToRedeem; |
||
| 210 |
uint256 collSharesDrawn; |
||
| 211 |
uint256 collSurplus; |
||
| 212 |
uint256 liquidatorRewardShares; |
||
| 213 |
bool cancelledPartial; |
||
| 214 |
bool fullRedemption; |
||
| 215 |
} |
||
| 216 | |||
| 217 |
function totalStakes() external view returns (uint256); |
||
| 218 | |||
| 219 |
function ebtcToken() external view returns (IEBTCToken); |
||
| 220 | |||
| 221 |
function systemStEthFeePerUnitIndex() external view returns (uint256); |
||
| 222 | |||
| 223 |
function systemStEthFeePerUnitIndexError() external view returns (uint256); |
||
| 224 | |||
| 225 |
function stEthIndex() external view returns (uint256); |
||
| 226 | |||
| 227 |
function calcFeeUponStakingReward( |
||
| 228 |
uint256 _newIndex, |
||
| 229 |
uint256 _prevIndex |
||
| 230 |
) external view returns (uint256, uint256, uint256); |
||
| 231 | |||
| 232 |
function syncGlobalAccounting() external; // Accrues StEthFeeSplit without influencing Grace Period |
||
| 233 | |||
| 234 |
function syncGlobalAccountingAndGracePeriod() external; // Accrues StEthFeeSplit and influences Grace Period |
||
| 235 | |||
| 236 |
function getAccumulatedFeeSplitApplied( |
||
| 237 |
bytes32 _cdpId, |
||
| 238 |
uint256 _systemStEthFeePerUnitIndex |
||
| 239 |
) external view returns (uint256, uint256); |
||
| 240 | |||
| 241 |
function getNominalICR(bytes32 _cdpId) external view returns (uint256); |
||
| 242 | |||
| 243 |
function getICR(bytes32 _cdpId, uint256 _price) external view returns (uint256); |
||
| 244 | |||
| 245 |
function getSyncedCdpDebt(bytes32 _cdpId) external view returns (uint256); |
||
| 246 | |||
| 247 |
function getSyncedCdpCollShares(bytes32 _cdpId) external view returns (uint256); |
||
| 248 | |||
| 249 |
function getSyncedICR(bytes32 _cdpId, uint256 _price) external view returns (uint256); |
||
| 250 | |||
| 251 |
function getSyncedTCR(uint256 _price) external view returns (uint256); |
||
| 252 | |||
| 253 |
function getPendingRedistributedDebt(bytes32 _cdpId) external view returns (uint256); |
||
| 254 | |||
| 255 |
function hasPendingRedistributedDebt(bytes32 _cdpId) external view returns (bool); |
||
| 256 | |||
| 257 |
function getDebtAndCollShares( |
||
| 258 |
bytes32 _cdpId |
||
| 259 |
) external view returns (uint256 debt, uint256 coll, uint256 pendingEBTCDebtReward); |
||
| 260 | |||
| 261 |
function canLiquidateRecoveryMode(uint256 icr, uint256 tcr) external view returns (bool); |
||
| 262 |
} |
||
| 263 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
interface ICollSurplusPool {
|
||
| 6 |
// --- Events --- |
||
| 7 | |||
| 8 |
event SurplusCollSharesUpdated(address indexed _account, uint256 _newBalance); |
||
| 9 |
event CollSharesTransferred(address _to, uint256 _amount); |
||
| 10 | |||
| 11 |
event SweepTokenSuccess(address indexed _token, uint256 _amount, address indexed _recipient); |
||
| 12 | |||
| 13 |
// --- Contract setters --- |
||
| 14 | |||
| 15 |
function getTotalSurplusCollShares() external view returns (uint256); |
||
| 16 | |||
| 17 |
function getSurplusCollShares(address _account) external view returns (uint256); |
||
| 18 | |||
| 19 |
function increaseSurplusCollShares(address _account, uint256 _amount) external; |
||
| 20 | |||
| 21 |
function claimSurplusCollShares(address _account) external; |
||
| 22 | |||
| 23 |
function increaseTotalSurplusCollShares(uint256 _value) external; |
||
| 24 |
} |
||
| 25 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "../Dependencies/IERC20.sol"; |
||
| 6 |
import "../Dependencies/IERC2612.sol"; |
||
| 7 | |||
| 8 |
interface IEBTCToken is IERC20, IERC2612 {
|
||
| 9 |
// --- Functions --- |
||
| 10 | |||
| 11 |
function mint(address _account, uint256 _amount) external; |
||
| 12 | |||
| 13 |
function burn(address _account, uint256 _amount) external; |
||
| 14 |
} |
||
| 15 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
interface IERC3156FlashBorrower {
|
||
| 6 |
/** |
||
| 7 |
* @dev Receive a flash loan. |
||
| 8 |
* @param initiator The initiator of the loan. |
||
| 9 |
* @param token The loan currency. |
||
| 10 |
* @param amount The amount of tokens lent. |
||
| 11 |
* @param fee The additional amount of tokens to repay. |
||
| 12 |
* @param data Arbitrary data structure, intended to contain user-defined parameters. |
||
| 13 |
* @return The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan" |
||
| 14 |
*/ |
||
| 15 |
function onFlashLoan( |
||
| 16 |
address initiator, |
||
| 17 |
address token, |
||
| 18 |
uint256 amount, |
||
| 19 |
uint256 fee, |
||
| 20 |
bytes calldata data |
||
| 21 |
) external returns (bytes32); |
||
| 22 |
} |
||
| 23 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./IERC3156FlashBorrower.sol"; |
||
| 6 | |||
| 7 |
interface IERC3156FlashLender {
|
||
| 8 |
event FlashFeeSet(address _setter, uint256 _oldFee, uint256 _newFee); |
||
| 9 |
event MaxFlashFeeSet(address _setter, uint256 _oldMaxFee, uint256 _newMaxFee); |
||
| 10 |
event FlashLoansPaused(address _setter, bool _paused); |
||
| 11 | |||
| 12 |
/** |
||
| 13 |
* @dev The amount of currency available to be lent. |
||
| 14 |
* @param token The loan currency. |
||
| 15 |
* @return The amount of `token` that can be borrowed. |
||
| 16 |
*/ |
||
| 17 |
function maxFlashLoan(address token) external view returns (uint256); |
||
| 18 | |||
| 19 |
/** |
||
| 20 |
* @dev The fee to be charged for a given loan. |
||
| 21 |
* @param token The loan currency. |
||
| 22 |
* @param amount The amount of tokens lent. |
||
| 23 |
* @return The amount of `token` to be charged for the loan, on top of the returned principal. |
||
| 24 |
*/ |
||
| 25 |
function flashFee(address token, uint256 amount) external view returns (uint256); |
||
| 26 | |||
| 27 |
/** |
||
| 28 |
* @dev Initiate a flash loan. |
||
| 29 |
* @param receiver The receiver of the tokens in the loan, and the receiver of the callback. |
||
| 30 |
* @param token The loan currency. |
||
| 31 |
* @param amount The amount of tokens lent. |
||
| 32 |
* @param data Arbitrary data structure, intended to contain user-defined parameters. |
||
| 33 |
*/ |
||
| 34 |
function flashLoan( |
||
| 35 |
IERC3156FlashBorrower receiver, |
||
| 36 |
address token, |
||
| 37 |
uint256 amount, |
||
| 38 |
bytes calldata data |
||
| 39 |
) external returns (bool); |
||
| 40 |
} |
||
| 41 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./IPriceFeed.sol"; |
||
| 6 | |||
| 7 |
interface IEbtcBase {
|
||
| 8 |
function priceFeed() external view returns (IPriceFeed); |
||
| 9 |
} |
||
| 10 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
interface IFallbackCaller {
|
||
| 6 |
// --- Events --- |
||
| 7 |
event FallbackTimeOutChanged(uint256 _oldTimeOut, uint256 _newTimeOut); |
||
| 8 | |||
| 9 |
// --- Function External View --- |
||
| 10 | |||
| 11 |
// NOTE: The fallback oracle must always return its answer scaled to 18 decimals where applicable |
||
| 12 |
// The system will assume an 18 decimal response for efficiency. |
||
| 13 |
function getFallbackResponse() external view returns (uint256, uint256, bool); |
||
| 14 | |||
| 15 |
// NOTE: this returns the timeout window interval for the fallback oracle instead |
||
| 16 |
// of storing in the `PriceFeed` contract is retrieve for the `FallbackCaller` |
||
| 17 |
function fallbackTimeout() external view returns (uint256); |
||
| 18 | |||
| 19 |
// --- Function External Setter --- |
||
| 20 | |||
| 21 |
function setFallbackTimeout(uint256 newFallbackTimeout) external; |
||
| 22 |
} |
||
| 23 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
interface IPermitNonce {
|
||
| 6 |
// --- Functions --- |
||
| 7 |
function increasePermitNonce() external returns (uint256); |
||
| 8 | |||
| 9 |
function nonces(address owner) external view returns (uint256); |
||
| 10 |
} |
||
| 11 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
// Common interface for the Pools. |
||
| 6 |
interface IPool {
|
||
| 7 |
// --- Events --- |
||
| 8 | |||
| 9 |
event ETHBalanceUpdated(uint256 _newBalance); |
||
| 10 |
event EBTCBalanceUpdated(uint256 _newBalance); |
||
| 11 |
event CollSharesTransferred(address _to, uint256 _amount); |
||
| 12 | |||
| 13 |
// --- Functions --- |
||
| 14 | |||
| 15 |
function getSystemCollShares() external view returns (uint256); |
||
| 16 | |||
| 17 |
function getSystemDebt() external view returns (uint256); |
||
| 18 | |||
| 19 |
function increaseSystemDebt(uint256 _amount) external; |
||
| 20 | |||
| 21 |
function decreaseSystemDebt(uint256 _amount) external; |
||
| 22 |
} |
||
| 23 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
interface IPositionManagers {
|
||
| 6 |
enum PositionManagerApproval {
|
||
| 7 |
None, |
||
| 8 |
OneTime, |
||
| 9 |
Persistent |
||
| 10 |
} |
||
| 11 | |||
| 12 |
event PositionManagerApprovalSet( |
||
| 13 |
address _borrower, |
||
| 14 |
address _positionManager, |
||
| 15 |
PositionManagerApproval _approval |
||
| 16 |
); |
||
| 17 | |||
| 18 |
function getPositionManagerApproval( |
||
| 19 |
address _borrower, |
||
| 20 |
address _positionManager |
||
| 21 |
) external view returns (PositionManagerApproval); |
||
| 22 | |||
| 23 |
function setPositionManagerApproval( |
||
| 24 |
address _positionManager, |
||
| 25 |
PositionManagerApproval _approval |
||
| 26 |
) external; |
||
| 27 | |||
| 28 |
function revokePositionManagerApproval(address _positionManager) external; |
||
| 29 | |||
| 30 |
function renouncePositionManagerApproval(address _borrower) external; |
||
| 31 | |||
| 32 |
function permitPositionManagerApproval( |
||
| 33 |
address _borrower, |
||
| 34 |
address _positionManager, |
||
| 35 |
PositionManagerApproval _approval, |
||
| 36 |
uint _deadline, |
||
| 37 |
uint8 v, |
||
| 38 |
bytes32 r, |
||
| 39 |
bytes32 s |
||
| 40 |
) external; |
||
| 41 | |||
| 42 |
function version() external view returns (string memory); |
||
| 43 | |||
| 44 |
function permitTypeHash() external view returns (bytes32); |
||
| 45 | |||
| 46 |
function domainSeparator() external view returns (bytes32); |
||
| 47 |
} |
||
| 48 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
interface IPriceFeed {
|
||
| 6 |
// --- Events --- |
||
| 7 |
event LastGoodPriceUpdated(uint256 _lastGoodPrice); |
||
| 8 |
event PriceFeedStatusChanged(Status newStatus); |
||
| 9 |
event FallbackCallerChanged(address _oldFallbackCaller, address _newFallbackCaller); |
||
| 10 |
event UnhealthyFallbackCaller(address _fallbackCaller, uint256 timestamp); |
||
| 11 | |||
| 12 |
// --- Structs --- |
||
| 13 | |||
| 14 |
struct ChainlinkResponse {
|
||
| 15 |
uint80 roundEthBtcId; |
||
| 16 |
uint80 roundStEthEthId; |
||
| 17 |
uint256 answer; |
||
| 18 |
uint256 timestampEthBtc; |
||
| 19 |
uint256 timestampStEthEth; |
||
| 20 |
bool success; |
||
| 21 |
} |
||
| 22 | |||
| 23 |
struct FallbackResponse {
|
||
| 24 |
uint256 answer; |
||
| 25 |
uint256 timestamp; |
||
| 26 |
bool success; |
||
| 27 |
} |
||
| 28 | |||
| 29 |
// --- Enum --- |
||
| 30 | |||
| 31 |
enum Status {
|
||
| 32 |
chainlinkWorking, |
||
| 33 |
usingFallbackChainlinkUntrusted, |
||
| 34 |
bothOraclesUntrusted, |
||
| 35 |
usingFallbackChainlinkFrozen, |
||
| 36 |
usingChainlinkFallbackUntrusted |
||
| 37 |
} |
||
| 38 | |||
| 39 |
// --- Function --- |
||
| 40 |
function fetchPrice() external returns (uint256); |
||
| 41 |
} |
||
| 42 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
// Interface for State Updates that can trigger RM Liquidations |
||
| 5 |
interface IRecoveryModeGracePeriod {
|
||
| 6 |
event TCRNotified(uint256 TCR); /// NOTE: Mostly for debugging to ensure synch |
||
| 7 | |||
| 8 |
// NOTE: Ts is implicit in events (it's added by GETH) |
||
| 9 |
event GracePeriodStart(); |
||
| 10 |
event GracePeriodEnd(); |
||
| 11 |
event GracePeriodDurationSet(uint256 _recoveryModeGracePeriodDuration); |
||
| 12 | |||
| 13 |
function notifyStartGracePeriod(uint256 tcr) external; |
||
| 14 | |||
| 15 |
function notifyEndGracePeriod(uint256 tcr) external; |
||
| 16 |
} |
||
| 17 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
// Common interface for the SortedCdps Doubly Linked List. |
||
| 6 |
interface ISortedCdps {
|
||
| 7 |
// --- Events --- |
||
| 8 | |||
| 9 |
event NodeAdded(bytes32 _id, uint _NICR); |
||
| 10 |
event NodeRemoved(bytes32 _id); |
||
| 11 | |||
| 12 |
// --- Functions --- |
||
| 13 | |||
| 14 |
function remove(bytes32 _id) external; |
||
| 15 | |||
| 16 |
function batchRemove(bytes32[] memory _ids) external; |
||
| 17 | |||
| 18 |
function reInsert(bytes32 _id, uint256 _newICR, bytes32 _prevId, bytes32 _nextId) external; |
||
| 19 | |||
| 20 |
function contains(bytes32 _id) external view returns (bool); |
||
| 21 | |||
| 22 |
function isFull() external view returns (bool); |
||
| 23 | |||
| 24 |
function isEmpty() external view returns (bool); |
||
| 25 | |||
| 26 |
function getSize() external view returns (uint256); |
||
| 27 | |||
| 28 |
function getMaxSize() external view returns (uint256); |
||
| 29 | |||
| 30 |
function getFirst() external view returns (bytes32); |
||
| 31 | |||
| 32 |
function getLast() external view returns (bytes32); |
||
| 33 | |||
| 34 |
function getNext(bytes32 _id) external view returns (bytes32); |
||
| 35 | |||
| 36 |
function getPrev(bytes32 _id) external view returns (bytes32); |
||
| 37 | |||
| 38 |
function validInsertPosition( |
||
| 39 |
uint256 _ICR, |
||
| 40 |
bytes32 _prevId, |
||
| 41 |
bytes32 _nextId |
||
| 42 |
) external view returns (bool); |
||
| 43 | |||
| 44 |
function findInsertPosition( |
||
| 45 |
uint256 _ICR, |
||
| 46 |
bytes32 _prevId, |
||
| 47 |
bytes32 _nextId |
||
| 48 |
) external view returns (bytes32, bytes32); |
||
| 49 | |||
| 50 |
function insert( |
||
| 51 |
address owner, |
||
| 52 |
uint256 _ICR, |
||
| 53 |
bytes32 _prevId, |
||
| 54 |
bytes32 _nextId |
||
| 55 |
) external returns (bytes32); |
||
| 56 | |||
| 57 |
function getOwnerAddress(bytes32 _id) external pure returns (address); |
||
| 58 | |||
| 59 |
function nonExistId() external view returns (bytes32); |
||
| 60 | |||
| 61 |
function cdpCountOf(address owner) external view returns (uint256); |
||
| 62 | |||
| 63 |
function getCdpCountOf( |
||
| 64 |
address owner, |
||
| 65 |
bytes32 startNodeId, |
||
| 66 |
uint maxNodes |
||
| 67 |
) external view returns (uint256, bytes32); |
||
| 68 | |||
| 69 |
function getCdpsOf(address owner) external view returns (bytes32[] memory); |
||
| 70 | |||
| 71 |
function getAllCdpsOf( |
||
| 72 |
address owner, |
||
| 73 |
bytes32 startNodeId, |
||
| 74 |
uint maxNodes |
||
| 75 |
) external view returns (bytes32[] memory, uint256, bytes32); |
||
| 76 | |||
| 77 |
function cdpOfOwnerByIndex(address owner, uint256 index) external view returns (bytes32); |
||
| 78 | |||
| 79 |
function cdpOfOwnerByIdx( |
||
| 80 |
address owner, |
||
| 81 |
uint256 index, |
||
| 82 |
bytes32 startNodeId, |
||
| 83 |
uint maxNodes |
||
| 84 |
) external view returns (bytes32, bool); |
||
| 85 |
} |
||
| 86 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
interface IWETH {
|
||
| 6 |
function deposit() external payable; |
||
| 7 | |||
| 8 |
function withdraw(uint256) external; |
||
| 9 | |||
| 10 |
function transfer(address to, uint256 amount) external returns (bool); |
||
| 11 | |||
| 12 |
function transferFrom(address from, address to, uint256 amount) external returns (bool); |
||
| 13 |
} |
||
| 14 |
| Lines covered: | 141 / 447 (31.5%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 |
import "./Interfaces/ICdpManagerData.sol"; |
||
| 5 |
import "./Interfaces/ICollSurplusPool.sol"; |
||
| 6 |
import "./Interfaces/IEBTCToken.sol"; |
||
| 7 |
import "./Interfaces/ISortedCdps.sol"; |
||
| 8 |
import "./Dependencies/ICollateralTokenOracle.sol"; |
||
| 9 |
import "./CdpManagerStorage.sol"; |
||
| 10 | |||
| 11 |
contract LiquidationLibrary is CdpManagerStorage {
|
||
| 12 |
constructor( |
||
| 13 |
address _borrowerOperationsAddress, |
||
| 14 |
address _collSurplusPool, |
||
| 15 |
address _ebtcToken, |
||
| 16 |
address _sortedCdps, |
||
| 17 |
address _activePool, |
||
| 18 |
address _priceFeed, |
||
| 19 |
address _collateral |
||
| 20 |
) |
||
| 21 |
CdpManagerStorage( |
||
| 22 |
√
|
address(0), |
|
| 23 |
√
|
address(0), |
|
| 24 |
√
|
_borrowerOperationsAddress, |
|
| 25 |
√
|
_collSurplusPool, |
|
| 26 |
√
|
_ebtcToken, |
|
| 27 |
√
|
_sortedCdps, |
|
| 28 |
√
|
_activePool, |
|
| 29 |
√
|
_priceFeed, |
|
| 30 |
√
|
_collateral |
|
| 31 |
) |
||
| 32 |
{}
|
||
| 33 | |||
| 34 |
/// @notice Single CDP liquidation function (fully). |
||
| 35 |
/// @notice callable by anyone, attempts to liquidate the CdpId. Executes successfully if Cdp meets the conditions for liquidation (e.g. in Normal Mode, it liquidates if the Cdp's ICR < the system MCR). |
||
| 36 |
function liquidate(bytes32 _cdpId) external nonReentrantSelfAndBOps {
|
||
| 37 |
⟳
|
_liquidateIndividualCdpSetup(_cdpId, 0, _cdpId, _cdpId); |
|
| 38 |
} |
||
| 39 | |||
| 40 |
// Single CDP liquidation function (partially). |
||
| 41 |
function partiallyLiquidate( |
||
| 42 |
bytes32 _cdpId, |
||
| 43 |
uint256 _partialAmount, |
||
| 44 |
bytes32 _upperPartialHint, |
||
| 45 |
bytes32 _lowerPartialHint |
||
| 46 |
) external nonReentrantSelfAndBOps {
|
||
| 47 |
√
|
⟳
|
require(_partialAmount != 0, "LiquidationLibrary: use `liquidate` for 100%"); |
| 48 |
√
|
⟳
|
_liquidateIndividualCdpSetup(_cdpId, _partialAmount, _upperPartialHint, _lowerPartialHint); |
| 49 |
} |
||
| 50 | |||
| 51 |
// Single CDP liquidation function. |
||
| 52 |
function _liquidateIndividualCdpSetup( |
||
| 53 |
bytes32 _cdpId, |
||
| 54 |
uint256 _partialAmount, |
||
| 55 |
bytes32 _upperPartialHint, |
||
| 56 |
bytes32 _lowerPartialHint |
||
| 57 |
) internal {
|
||
| 58 |
√
|
⟳
|
_requireCdpIsActive(_cdpId); |
| 59 | |||
| 60 |
√
|
⟳
|
_syncAccounting(_cdpId); |
| 61 | |||
| 62 |
√
|
⟳
|
uint256 _price = priceFeed.fetchPrice(); |
| 63 | |||
| 64 |
// prepare local variables |
||
| 65 |
√
|
⟳
|
uint256 _ICR = getICR(_cdpId, _price); // @audit syncAccounting already called, guarenteed to be synced |
| 66 |
√
|
⟳
|
(uint256 _TCR, uint256 systemColl, uint256 systemDebt) = _getTCRWithSystemDebtAndCollShares( |
| 67 |
√
|
⟳
|
_price |
| 68 |
); |
||
| 69 | |||
| 70 |
// If CDP is above MCR |
||
| 71 |
√
|
⟳
|
if (_ICR >= MCR) {
|
| 72 |
// We must be in RM |
||
| 73 |
require( |
||
| 74 |
⟳
|
_checkICRAgainstLiqThreshold(_ICR, _TCR), |
|
| 75 |
"LiquidationLibrary: ICR is not below liquidation threshold in current mode" |
||
| 76 |
); |
||
| 77 | |||
| 78 |
// == Grace Period == // |
||
| 79 |
uint128 cachedLastGracePeriodStartTimestamp = lastGracePeriodStartTimestamp; |
||
| 80 |
require( |
||
| 81 |
cachedLastGracePeriodStartTimestamp != UNSET_TIMESTAMP, |
||
| 82 |
"LiquidationLibrary: Recovery Mode grace period not started" |
||
| 83 |
); |
||
| 84 |
require( |
||
| 85 |
block.timestamp > |
||
| 86 |
cachedLastGracePeriodStartTimestamp + recoveryModeGracePeriodDuration, |
||
| 87 |
"LiquidationLibrary: Recovery mode grace period still in effect" |
||
| 88 |
); |
||
| 89 |
} // Implicit Else Case, Implies ICR < MRC, meaning the CDP is liquidatable |
||
| 90 | |||
| 91 |
√
|
bool _recoveryModeAtStart = _TCR < CCR ? true : false; |
|
| 92 |
√
|
LiquidationLocals memory _liqState = LiquidationLocals( |
|
| 93 |
√
|
_cdpId, |
|
| 94 |
√
|
_partialAmount, |
|
| 95 |
√
|
_price, |
|
| 96 |
√
|
_ICR, |
|
| 97 |
√
|
_upperPartialHint, |
|
| 98 |
√
|
_lowerPartialHint, |
|
| 99 |
√
|
(_recoveryModeAtStart), |
|
| 100 |
√
|
_TCR, |
|
| 101 |
√
|
0, |
|
| 102 |
√
|
0, |
|
| 103 |
√
|
0, |
|
| 104 |
√
|
0, |
|
| 105 |
√
|
0 |
|
| 106 |
); |
||
| 107 | |||
| 108 |
√
|
LiquidationRecoveryModeLocals memory _rs = LiquidationRecoveryModeLocals( |
|
| 109 |
√
|
systemDebt, |
|
| 110 |
√
|
systemColl, |
|
| 111 |
√
|
0, |
|
| 112 |
√
|
0, |
|
| 113 |
√
|
0, |
|
| 114 |
√
|
_cdpId, |
|
| 115 |
√
|
_price, |
|
| 116 |
√
|
_ICR, |
|
| 117 |
√
|
0, |
|
| 118 |
√
|
0 |
|
| 119 |
); |
||
| 120 | |||
| 121 |
√
|
_liquidateIndividualCdpSetupCDP(_liqState, _rs); |
|
| 122 |
} |
||
| 123 | |||
| 124 |
// liquidate given CDP by repaying debt in full or partially if its ICR is below MCR or TCR in recovery mode. |
||
| 125 |
// For partial liquidation, caller should use HintHelper smart contract to get correct hints for reinsertion into sorted CDP list |
||
| 126 |
function _liquidateIndividualCdpSetupCDP( |
||
| 127 |
LiquidationLocals memory _liqState, |
||
| 128 |
LiquidationRecoveryModeLocals memory _recoveryState |
||
| 129 |
) internal {
|
||
| 130 |
√
|
LiquidationValues memory liquidationValues; |
|
| 131 | |||
| 132 |
√
|
uint256 startingSystemDebt = _recoveryState.entireSystemDebt; |
|
| 133 |
√
|
uint256 startingSystemColl = _recoveryState.entireSystemColl; |
|
| 134 | |||
| 135 |
√
|
if (_liqState.partialAmount == 0) {
|
|
| 136 |
( |
||
| 137 |
liquidationValues.debtToBurn, |
||
| 138 |
liquidationValues.totalCollToSendToLiquidator, |
||
| 139 |
liquidationValues.debtToRedistribute, |
||
| 140 |
liquidationValues.liquidatorCollSharesReward, |
||
| 141 |
liquidationValues.collSurplus |
||
| 142 |
) = _liquidateCdpInGivenMode(_liqState, _recoveryState); |
||
| 143 |
} else {
|
||
| 144 |
( |
||
| 145 |
√
|
liquidationValues.debtToBurn, |
|
| 146 |
√
|
liquidationValues.totalCollToSendToLiquidator |
|
| 147 |
√
|
) = _liquidateCDPPartially(_liqState); |
|
| 148 |
if ( |
||
| 149 |
√
|
liquidationValues.totalCollToSendToLiquidator == 0 && |
|
| 150 |
liquidationValues.debtToBurn == 0 |
||
| 151 |
) {
|
||
| 152 |
// retry with fully liquidation |
||
| 153 |
( |
||
| 154 |
liquidationValues.debtToBurn, |
||
| 155 |
liquidationValues.totalCollToSendToLiquidator, |
||
| 156 |
liquidationValues.debtToRedistribute, |
||
| 157 |
liquidationValues.liquidatorCollSharesReward, |
||
| 158 |
liquidationValues.collSurplus |
||
| 159 |
) = _liquidateCdpInGivenMode(_liqState, _recoveryState); |
||
| 160 |
} |
||
| 161 |
} |
||
| 162 | |||
| 163 |
√
|
_finalizeLiquidation( |
|
| 164 |
√
|
liquidationValues.debtToBurn, |
|
| 165 |
√
|
liquidationValues.totalCollToSendToLiquidator, |
|
| 166 |
√
|
liquidationValues.debtToRedistribute, |
|
| 167 |
√
|
liquidationValues.liquidatorCollSharesReward, |
|
| 168 |
√
|
liquidationValues.collSurplus, |
|
| 169 |
√
|
startingSystemColl, |
|
| 170 |
√
|
startingSystemDebt, |
|
| 171 |
√
|
_liqState.price |
|
| 172 |
); |
||
| 173 |
} |
||
| 174 | |||
| 175 |
// liquidate (and close) the CDP from an external liquidator |
||
| 176 |
// this function would return the liquidated debt and collateral of the given CDP |
||
| 177 |
function _liquidateCdpInGivenMode( |
||
| 178 |
LiquidationLocals memory _liqState, |
||
| 179 |
LiquidationRecoveryModeLocals memory _recoveryState |
||
| 180 |
) private returns (uint256, uint256, uint256, uint256, uint256) {
|
||
| 181 |
if (_liqState.recoveryModeAtStart) {
|
||
| 182 |
LiquidationRecoveryModeLocals |
||
| 183 |
memory _outputState = _liquidateIndividualCdpSetupCDPInRecoveryMode(_recoveryState); |
||
| 184 | |||
| 185 |
// housekeeping leftover collateral for liquidated CDP |
||
| 186 |
if (_outputState.totalSurplusCollShares > 0) {
|
||
| 187 |
activePool.transferSystemCollShares( |
||
| 188 |
address(collSurplusPool), |
||
| 189 |
_outputState.totalSurplusCollShares |
||
| 190 |
); |
||
| 191 |
} |
||
| 192 | |||
| 193 |
return ( |
||
| 194 |
_outputState.totalDebtToBurn, |
||
| 195 |
_outputState.totalCollSharesToSend, |
||
| 196 |
_outputState.totalDebtToRedistribute, |
||
| 197 |
_outputState.totalLiquidatorRewardCollShares, |
||
| 198 |
_outputState.totalSurplusCollShares |
||
| 199 |
); |
||
| 200 |
} else {
|
||
| 201 |
LiquidationLocals memory _outputState = _liquidateIndividualCdpSetupCDPInNormalMode( |
||
| 202 |
_liqState |
||
| 203 |
); |
||
| 204 |
return ( |
||
| 205 |
_outputState.totalDebtToBurn, |
||
| 206 |
_outputState.totalCollSharesToSend, |
||
| 207 |
_outputState.totalDebtToRedistribute, |
||
| 208 |
_outputState.totalLiquidatorRewardCollShares, |
||
| 209 |
_outputState.totalSurplusCollShares |
||
| 210 |
); |
||
| 211 |
} |
||
| 212 |
} |
||
| 213 | |||
| 214 |
function _liquidateIndividualCdpSetupCDPInNormalMode( |
||
| 215 |
LiquidationLocals memory _liqState |
||
| 216 |
) private returns (LiquidationLocals memory) {
|
||
| 217 |
// liquidate entire debt |
||
| 218 |
( |
||
| 219 |
uint256 _totalDebtToBurn, |
||
| 220 |
uint256 _totalColToSend, |
||
| 221 |
uint256 _liquidatorReward |
||
| 222 |
) = _closeCdpByLiquidation(_liqState.cdpId); |
||
| 223 |
uint256 _cappedColPortion; |
||
| 224 |
uint256 _collSurplus; |
||
| 225 |
uint256 _debtToRedistribute; |
||
| 226 |
address _borrower = sortedCdps.getOwnerAddress(_liqState.cdpId); |
||
| 227 | |||
| 228 |
// I don't see an issue emitting the CdpUpdated() event up here and avoiding this extra cache, any objections? |
||
| 229 |
emit CdpUpdated( |
||
| 230 |
_liqState.cdpId, |
||
| 231 |
_borrower, |
||
| 232 |
_totalDebtToBurn, |
||
| 233 |
_totalColToSend, |
||
| 234 |
0, |
||
| 235 |
0, |
||
| 236 |
0, |
||
| 237 |
CdpOperation.liquidateInNormalMode |
||
| 238 |
); |
||
| 239 | |||
| 240 |
{
|
||
| 241 |
(_cappedColPortion, _collSurplus, _debtToRedistribute) = _calculateSurplusAndCap( |
||
| 242 |
_liqState.ICR, |
||
| 243 |
_liqState.price, |
||
| 244 |
_totalDebtToBurn, |
||
| 245 |
_totalColToSend, |
||
| 246 |
true |
||
| 247 |
); |
||
| 248 |
if (_collSurplus > 0) {
|
||
| 249 |
// due to division precision loss, should be zero surplus in normal mode |
||
| 250 |
_cappedColPortion = _cappedColPortion + _collSurplus; |
||
| 251 |
_collSurplus = 0; |
||
| 252 |
} |
||
| 253 |
if (_debtToRedistribute > 0) {
|
||
| 254 |
_totalDebtToBurn = _totalDebtToBurn - _debtToRedistribute; |
||
| 255 |
} |
||
| 256 |
} |
||
| 257 |
_liqState.totalDebtToBurn = _liqState.totalDebtToBurn + _totalDebtToBurn; |
||
| 258 |
_liqState.totalCollSharesToSend = _liqState.totalCollSharesToSend + _cappedColPortion; |
||
| 259 |
_liqState.totalDebtToRedistribute = _liqState.totalDebtToRedistribute + _debtToRedistribute; |
||
| 260 |
_liqState.totalLiquidatorRewardCollShares = |
||
| 261 |
_liqState.totalLiquidatorRewardCollShares + |
||
| 262 |
_liquidatorReward; |
||
| 263 | |||
| 264 |
// Emit events |
||
| 265 |
uint _debtToColl = (_totalDebtToBurn * DECIMAL_PRECISION) / _liqState.price; |
||
| 266 |
uint _cappedColl = collateral.getPooledEthByShares(_cappedColPortion + _liquidatorReward); |
||
| 267 | |||
| 268 |
emit CdpLiquidated( |
||
| 269 |
_liqState.cdpId, |
||
| 270 |
_borrower, |
||
| 271 |
_totalDebtToBurn, |
||
| 272 |
// please note this is the collateral share of the liquidated CDP |
||
| 273 |
_cappedColPortion, |
||
| 274 |
CdpOperation.liquidateInNormalMode, |
||
| 275 |
msg.sender, |
||
| 276 |
_cappedColl > _debtToColl ? (_cappedColl - _debtToColl) : 0 |
||
| 277 |
); |
||
| 278 | |||
| 279 |
return _liqState; |
||
| 280 |
} |
||
| 281 | |||
| 282 |
function _liquidateIndividualCdpSetupCDPInRecoveryMode( |
||
| 283 |
LiquidationRecoveryModeLocals memory _recoveryState |
||
| 284 |
) private returns (LiquidationRecoveryModeLocals memory) {
|
||
| 285 |
// liquidate entire debt |
||
| 286 |
( |
||
| 287 |
uint256 _totalDebtToBurn, |
||
| 288 |
uint256 _totalColToSend, |
||
| 289 |
uint256 _liquidatorReward |
||
| 290 |
) = _closeCdpByLiquidation(_recoveryState.cdpId); |
||
| 291 | |||
| 292 |
// cap the liquidated collateral if required |
||
| 293 |
uint256 _cappedColPortion; |
||
| 294 |
uint256 _collSurplus; |
||
| 295 |
uint256 _debtToRedistribute; |
||
| 296 |
address _borrower = sortedCdps.getOwnerAddress(_recoveryState.cdpId); |
||
| 297 | |||
| 298 |
// I don't see an issue emitting the CdpUpdated() event up here and avoiding an extra cache of the values, any objections? |
||
| 299 |
emit CdpUpdated( |
||
| 300 |
_recoveryState.cdpId, |
||
| 301 |
_borrower, |
||
| 302 |
_totalDebtToBurn, |
||
| 303 |
_totalColToSend, |
||
| 304 |
0, |
||
| 305 |
0, |
||
| 306 |
0, |
||
| 307 |
CdpOperation.liquidateInRecoveryMode |
||
| 308 |
); |
||
| 309 | |||
| 310 |
// avoid stack too deep |
||
| 311 |
{
|
||
| 312 |
(_cappedColPortion, _collSurplus, _debtToRedistribute) = _calculateSurplusAndCap( |
||
| 313 |
_recoveryState.ICR, |
||
| 314 |
_recoveryState.price, |
||
| 315 |
_totalDebtToBurn, |
||
| 316 |
_totalColToSend, |
||
| 317 |
true |
||
| 318 |
); |
||
| 319 |
if (_collSurplus > 0) {
|
||
| 320 |
collSurplusPool.increaseSurplusCollShares(_borrower, _collSurplus); |
||
| 321 |
_recoveryState.totalSurplusCollShares = |
||
| 322 |
_recoveryState.totalSurplusCollShares + |
||
| 323 |
_collSurplus; |
||
| 324 |
} |
||
| 325 |
if (_debtToRedistribute > 0) {
|
||
| 326 |
_totalDebtToBurn = _totalDebtToBurn - _debtToRedistribute; |
||
| 327 |
} |
||
| 328 |
} |
||
| 329 |
_recoveryState.totalDebtToBurn = _recoveryState.totalDebtToBurn + _totalDebtToBurn; |
||
| 330 |
_recoveryState.totalCollSharesToSend = |
||
| 331 |
_recoveryState.totalCollSharesToSend + |
||
| 332 |
_cappedColPortion; |
||
| 333 |
_recoveryState.totalDebtToRedistribute = |
||
| 334 |
_recoveryState.totalDebtToRedistribute + |
||
| 335 |
_debtToRedistribute; |
||
| 336 |
_recoveryState.totalLiquidatorRewardCollShares = |
||
| 337 |
_recoveryState.totalLiquidatorRewardCollShares + |
||
| 338 |
_liquidatorReward; |
||
| 339 | |||
| 340 |
// check if system back to normal mode |
||
| 341 |
_recoveryState.entireSystemDebt = _recoveryState.entireSystemDebt > _totalDebtToBurn |
||
| 342 |
? _recoveryState.entireSystemDebt - _totalDebtToBurn |
||
| 343 |
: 0; |
||
| 344 |
_recoveryState.entireSystemColl = _recoveryState.entireSystemColl > _totalColToSend |
||
| 345 |
? _recoveryState.entireSystemColl - _totalColToSend |
||
| 346 |
: 0; |
||
| 347 | |||
| 348 |
uint _debtToColl = (_totalDebtToBurn * DECIMAL_PRECISION) / _recoveryState.price; |
||
| 349 |
uint _cappedColl = collateral.getPooledEthByShares(_cappedColPortion + _liquidatorReward); |
||
| 350 |
emit CdpLiquidated( |
||
| 351 |
_recoveryState.cdpId, |
||
| 352 |
_borrower, |
||
| 353 |
_totalDebtToBurn, |
||
| 354 |
// please note this is the collateral share of the liquidated CDP |
||
| 355 |
_cappedColPortion, |
||
| 356 |
CdpOperation.liquidateInRecoveryMode, |
||
| 357 |
msg.sender, |
||
| 358 |
_cappedColl > _debtToColl ? (_cappedColl - _debtToColl) : 0 |
||
| 359 |
); |
||
| 360 | |||
| 361 |
return _recoveryState; |
||
| 362 |
} |
||
| 363 | |||
| 364 |
// liquidate (and close) the CDP from an external liquidator |
||
| 365 |
// this function would return the liquidated debt and collateral of the given CDP |
||
| 366 |
// without emmiting events |
||
| 367 |
function _closeCdpByLiquidation(bytes32 _cdpId) private returns (uint256, uint256, uint256) {
|
||
| 368 |
// calculate entire debt to repay |
||
| 369 |
(uint256 entireDebt, uint256 entireColl, ) = getDebtAndCollShares(_cdpId); |
||
| 370 | |||
| 371 |
// housekeeping after liquidation by closing the CDP |
||
| 372 |
_removeStake(_cdpId); |
||
| 373 |
uint256 _liquidatorReward = Cdps[_cdpId].liquidatorRewardShares; |
||
| 374 |
_closeCdp(_cdpId, Status.closedByLiquidation); |
||
| 375 | |||
| 376 |
return (entireDebt, entireColl, _liquidatorReward); |
||
| 377 |
} |
||
| 378 | |||
| 379 |
// Liquidate partially the CDP by an external liquidator |
||
| 380 |
// This function would return the liquidated debt and collateral of the given CDP |
||
| 381 |
function _liquidateCDPPartially( |
||
| 382 |
LiquidationLocals memory _partialState |
||
| 383 |
√
|
) private returns (uint256, uint256) {
|
|
| 384 |
√
|
bytes32 _cdpId = _partialState.cdpId; |
|
| 385 |
√
|
uint256 _partialDebt = _partialState.partialAmount; |
|
| 386 | |||
| 387 |
// calculate entire debt to repay |
||
| 388 |
√
|
CdpDebtAndCollShares memory _debtAndColl = _getDebtAndCollShares(_cdpId); |
|
| 389 |
√
|
_requirePartialLiqDebtSize(_partialDebt, _debtAndColl.entireDebt, _partialState.price); |
|
| 390 |
√
|
uint256 newDebt = _debtAndColl.entireDebt - _partialDebt; |
|
| 391 | |||
| 392 |
// credit to https://arxiv.org/pdf/2212.07306.pdf for details |
||
| 393 |
√
|
(uint256 _partialColl, uint256 newColl, ) = _calculateSurplusAndCap( |
|
| 394 |
√
|
_partialState.ICR, |
|
| 395 |
√
|
_partialState.price, |
|
| 396 |
√
|
_partialDebt, |
|
| 397 |
√
|
_debtAndColl.entireColl, |
|
| 398 |
√
|
false |
|
| 399 |
); |
||
| 400 | |||
| 401 |
// early return: if new collateral is zero, we have a full liqudiation |
||
| 402 |
√
|
if (newColl == 0) {
|
|
| 403 |
return (0, 0); |
||
| 404 |
} |
||
| 405 | |||
| 406 |
// If we have coll remaining, it must meet minimum CDP size requirements |
||
| 407 |
√
|
_requirePartialLiqCollSize(collateral.getPooledEthByShares(newColl)); |
|
| 408 | |||
| 409 |
// updating the CDP accounting for partial liquidation |
||
| 410 |
√
|
_partiallyReduceCdpDebt(_cdpId, _partialDebt, _partialColl); |
|
| 411 | |||
| 412 |
// reInsert into sorted CDP list after partial liquidation |
||
| 413 |
{
|
||
| 414 |
√
|
_reInsertPartialLiquidation( |
|
| 415 |
√
|
_partialState, |
|
| 416 |
√
|
EbtcMath._computeNominalCR(newColl, newDebt), |
|
| 417 |
√
|
_debtAndColl.entireDebt, |
|
| 418 |
√
|
_debtAndColl.entireColl |
|
| 419 |
); |
||
| 420 |
√
|
uint _debtToColl = (_partialDebt * DECIMAL_PRECISION) / _partialState.price; |
|
| 421 |
√
|
uint _cappedColl = collateral.getPooledEthByShares(_partialColl); |
|
| 422 |
emit CdpPartiallyLiquidated( |
||
| 423 |
√
|
_cdpId, |
|
| 424 |
√
|
sortedCdps.getOwnerAddress(_cdpId), |
|
| 425 |
√
|
_partialDebt, |
|
| 426 |
√
|
_partialColl, |
|
| 427 |
√
|
CdpOperation.partiallyLiquidate, |
|
| 428 |
√
|
msg.sender, |
|
| 429 |
√
|
_cappedColl > _debtToColl ? (_cappedColl - _debtToColl) : 0 |
|
| 430 |
); |
||
| 431 |
} |
||
| 432 |
√
|
return (_partialDebt, _partialColl); |
|
| 433 |
} |
||
| 434 | |||
| 435 |
function _partiallyReduceCdpDebt( |
||
| 436 |
bytes32 _cdpId, |
||
| 437 |
uint256 _partialDebt, |
||
| 438 |
uint256 _partialColl |
||
| 439 |
) internal {
|
||
| 440 |
√
|
Cdp storage _cdp = Cdps[_cdpId]; |
|
| 441 | |||
| 442 |
√
|
uint256 _coll = _cdp.coll; |
|
| 443 |
√
|
uint256 _debt = _cdp.debt; |
|
| 444 | |||
| 445 |
√
|
_cdp.coll = _coll - _partialColl; |
|
| 446 |
√
|
_cdp.debt = _debt - _partialDebt; |
|
| 447 |
√
|
_updateStakeAndTotalStakes(_cdpId); |
|
| 448 | |||
| 449 |
√
|
_updateRedistributedDebtSnapshot(_cdpId); |
|
| 450 |
} |
||
| 451 | |||
| 452 |
// Re-Insertion into SortedCdp list after partial liquidation |
||
| 453 |
function _reInsertPartialLiquidation( |
||
| 454 |
LiquidationLocals memory _partialState, |
||
| 455 |
uint256 _newNICR, |
||
| 456 |
uint256 _oldDebt, |
||
| 457 |
uint256 _oldColl |
||
| 458 |
) internal {
|
||
| 459 |
√
|
bytes32 _cdpId = _partialState.cdpId; |
|
| 460 | |||
| 461 |
// ensure new ICR does NOT decrease due to partial liquidation |
||
| 462 |
// if original ICR is above LICR |
||
| 463 |
√
|
if (_partialState.ICR > LICR) {
|
|
| 464 |
require( |
||
| 465 |
√
|
getICR(_cdpId, _partialState.price) >= _partialState.ICR, |
|
| 466 |
"LiquidationLibrary: !_newICR>=_ICR" |
||
| 467 |
); |
||
| 468 |
} |
||
| 469 | |||
| 470 |
// reInsert into sorted CDP list |
||
| 471 |
√
|
sortedCdps.reInsert( |
|
| 472 |
√
|
_cdpId, |
|
| 473 |
√
|
_newNICR, |
|
| 474 |
√
|
_partialState.upperPartialHint, |
|
| 475 |
√
|
_partialState.lowerPartialHint |
|
| 476 |
); |
||
| 477 |
emit CdpUpdated( |
||
| 478 |
√
|
_cdpId, |
|
| 479 |
√
|
sortedCdps.getOwnerAddress(_cdpId), |
|
| 480 |
√
|
_oldDebt, |
|
| 481 |
√
|
_oldColl, |
|
| 482 |
√
|
Cdps[_cdpId].debt, |
|
| 483 |
√
|
Cdps[_cdpId].coll, |
|
| 484 |
√
|
Cdps[_cdpId].stake, |
|
| 485 |
√
|
CdpOperation.partiallyLiquidate |
|
| 486 |
); |
||
| 487 |
} |
||
| 488 | |||
| 489 |
function _finalizeLiquidation( |
||
| 490 |
uint256 totalDebtToBurn, |
||
| 491 |
uint256 totalCollSharesToSend, |
||
| 492 |
uint256 totalDebtToRedistribute, |
||
| 493 |
uint256 totalLiquidatorRewardCollShares, |
||
| 494 |
uint256 totalSurplusCollShares, |
||
| 495 |
uint256 systemInitialCollShares, |
||
| 496 |
uint256 systemInitialDebt, |
||
| 497 |
uint256 price |
||
| 498 |
) internal {
|
||
| 499 |
// update the staking and collateral snapshots |
||
| 500 |
√
|
_updateSystemSnapshotsExcludeCollRemainder(totalCollSharesToSend); |
|
| 501 | |||
| 502 |
√
|
emit Liquidation(totalDebtToBurn, totalCollSharesToSend, totalLiquidatorRewardCollShares); |
|
| 503 | |||
| 504 |
√
|
_syncGracePeriodForGivenValues( |
|
| 505 |
√
|
systemInitialCollShares - totalCollSharesToSend - totalSurplusCollShares, |
|
| 506 |
√
|
systemInitialDebt - totalDebtToBurn, |
|
| 507 |
√
|
price |
|
| 508 |
); |
||
| 509 | |||
| 510 |
// redistribute debt if any |
||
| 511 |
√
|
if (totalDebtToRedistribute > 0) {
|
|
| 512 |
_redistributeDebt(totalDebtToRedistribute); |
||
| 513 |
} |
||
| 514 | |||
| 515 |
// burn the debt from liquidator |
||
| 516 |
√
|
ebtcToken.burn(msg.sender, totalDebtToBurn); |
|
| 517 | |||
| 518 |
// offset debt from Active Pool |
||
| 519 |
√
|
activePool.decreaseSystemDebt(totalDebtToBurn); |
|
| 520 | |||
| 521 |
// CEI: ensure sending back collateral to liquidator is last thing to do |
||
| 522 |
√
|
activePool.transferSystemCollSharesAndLiquidatorReward( |
|
| 523 |
√
|
msg.sender, |
|
| 524 |
√
|
totalCollSharesToSend, |
|
| 525 |
√
|
totalLiquidatorRewardCollShares |
|
| 526 |
); |
||
| 527 |
} |
||
| 528 | |||
| 529 |
// Function that calculates the amount of collateral to send to liquidator (plus incentive) and the amount of collateral surplus |
||
| 530 |
function _calculateSurplusAndCap( |
||
| 531 |
uint256 _ICR, |
||
| 532 |
uint256 _price, |
||
| 533 |
uint256 _totalDebtToBurn, |
||
| 534 |
uint256 _totalColToSend, |
||
| 535 |
bool _fullLiquidation |
||
| 536 |
) |
||
| 537 |
private |
||
| 538 |
view |
||
| 539 |
√
|
returns (uint256 cappedColPortion, uint256 collSurplus, uint256 debtToRedistribute) |
|
| 540 |
{
|
||
| 541 |
// Calculate liquidation incentive for liquidator: |
||
| 542 |
// If ICR is less than 103%: give away 103% worth of collateral to liquidator, i.e., repaidDebt * 103% / price |
||
| 543 |
// If ICR is more than 103%: give away min(ICR, 110%) worth of collateral to liquidator, i.e., repaidDebt * min(ICR, 110%) / price |
||
| 544 |
√
|
uint256 _incentiveColl; |
|
| 545 |
√
|
if (_ICR > LICR) {
|
|
| 546 |
√
|
_incentiveColl = (_totalDebtToBurn * (_ICR > MCR ? MCR : _ICR)) / _price; |
|
| 547 |
} else {
|
||
| 548 |
if (_fullLiquidation) {
|
||
| 549 |
// for full liquidation, there would be some bad debt to redistribute |
||
| 550 |
_incentiveColl = collateral.getPooledEthByShares(_totalColToSend); |
||
| 551 |
uint256 _debtToRepay = (_incentiveColl * _price) / LICR; |
||
| 552 |
debtToRedistribute = _debtToRepay < _totalDebtToBurn |
||
| 553 |
? _totalDebtToBurn - _debtToRepay |
||
| 554 |
: 0; |
||
| 555 |
// now CDP owner should have zero surplus to claim |
||
| 556 |
cappedColPortion = _totalColToSend; |
||
| 557 |
} else {
|
||
| 558 |
// for partial liquidation, new ICR would deteriorate |
||
| 559 |
// since we give more incentive (103%) than current _ICR allowed |
||
| 560 |
_incentiveColl = (_totalDebtToBurn * LICR) / _price; |
||
| 561 |
} |
||
| 562 |
} |
||
| 563 |
√
|
if (cappedColPortion == 0) {
|
|
| 564 |
√
|
cappedColPortion = collateral.getSharesByPooledEth(_incentiveColl); |
|
| 565 |
} |
||
| 566 |
√
|
cappedColPortion = cappedColPortion < _totalColToSend ? cappedColPortion : _totalColToSend; |
|
| 567 |
√
|
collSurplus = (cappedColPortion == _totalColToSend) ? 0 : _totalColToSend - cappedColPortion; |
|
| 568 |
} |
||
| 569 | |||
| 570 |
// --- Batch liquidation functions --- |
||
| 571 | |||
| 572 |
function _getLiquidationValuesNormalMode( |
||
| 573 |
uint256 _price, |
||
| 574 |
uint256 _TCR, |
||
| 575 |
LocalVariables_LiquidationSequence memory vars, |
||
| 576 |
LiquidationValues memory singleLiquidation |
||
| 577 |
) internal {
|
||
| 578 |
LiquidationLocals memory _liqState = LiquidationLocals( |
||
| 579 |
vars.cdpId, |
||
| 580 |
0, |
||
| 581 |
_price, |
||
| 582 |
vars.ICR, |
||
| 583 |
vars.cdpId, |
||
| 584 |
vars.cdpId, |
||
| 585 |
(false), |
||
| 586 |
_TCR, |
||
| 587 |
0, |
||
| 588 |
0, |
||
| 589 |
0, |
||
| 590 |
0, |
||
| 591 |
0 |
||
| 592 |
); |
||
| 593 | |||
| 594 |
LiquidationLocals memory _outputState = _liquidateIndividualCdpSetupCDPInNormalMode( |
||
| 595 |
_liqState |
||
| 596 |
); |
||
| 597 | |||
| 598 |
singleLiquidation.entireCdpDebt = _outputState.totalDebtToBurn; |
||
| 599 |
singleLiquidation.debtToBurn = _outputState.totalDebtToBurn; |
||
| 600 |
singleLiquidation.totalCollToSendToLiquidator = _outputState.totalCollSharesToSend; |
||
| 601 |
singleLiquidation.collSurplus = _outputState.totalSurplusCollShares; |
||
| 602 |
singleLiquidation.debtToRedistribute = _outputState.totalDebtToRedistribute; |
||
| 603 |
singleLiquidation.liquidatorCollSharesReward = _outputState.totalLiquidatorRewardCollShares; |
||
| 604 |
} |
||
| 605 | |||
| 606 |
function _getLiquidationValuesRecoveryMode( |
||
| 607 |
uint256 _price, |
||
| 608 |
uint256 _systemDebt, |
||
| 609 |
uint256 _systemCollShares, |
||
| 610 |
LocalVariables_LiquidationSequence memory vars, |
||
| 611 |
LiquidationValues memory singleLiquidation |
||
| 612 |
) internal {
|
||
| 613 |
LiquidationRecoveryModeLocals memory _recState = LiquidationRecoveryModeLocals( |
||
| 614 |
_systemDebt, |
||
| 615 |
_systemCollShares, |
||
| 616 |
0, |
||
| 617 |
0, |
||
| 618 |
0, |
||
| 619 |
vars.cdpId, |
||
| 620 |
_price, |
||
| 621 |
vars.ICR, |
||
| 622 |
0, |
||
| 623 |
0 |
||
| 624 |
); |
||
| 625 | |||
| 626 |
LiquidationRecoveryModeLocals |
||
| 627 |
memory _outputState = _liquidateIndividualCdpSetupCDPInRecoveryMode(_recState); |
||
| 628 | |||
| 629 |
singleLiquidation.entireCdpDebt = _outputState.totalDebtToBurn; |
||
| 630 |
singleLiquidation.debtToBurn = _outputState.totalDebtToBurn; |
||
| 631 |
singleLiquidation.totalCollToSendToLiquidator = _outputState.totalCollSharesToSend; |
||
| 632 |
singleLiquidation.collSurplus = _outputState.totalSurplusCollShares; |
||
| 633 |
singleLiquidation.debtToRedistribute = _outputState.totalDebtToRedistribute; |
||
| 634 |
singleLiquidation.liquidatorCollSharesReward = _outputState.totalLiquidatorRewardCollShares; |
||
| 635 |
} |
||
| 636 | |||
| 637 |
/* |
||
| 638 |
* Attempt to liquidate a custom list of cdps provided by the caller. |
||
| 639 | |||
| 640 |
callable by anyone, accepts a custom list of Cdps addresses as an argument. Steps through the provided list and attempts to liquidate every Cdp, until it reaches the end or it runs out of gas. A Cdp is liquidated only if it meets the conditions for liquidation. For a batch of 10 Cdps, the gas costs per liquidated Cdp are roughly between 75K-83K, for a batch of 50 Cdps between 54K-69K. |
||
| 641 |
*/ |
||
| 642 |
function batchLiquidateCdps(bytes32[] memory _cdpArray) external nonReentrantSelfAndBOps {
|
||
| 643 |
require( |
||
| 644 |
⟳
|
_cdpArray.length != 0, |
|
| 645 |
"LiquidationLibrary: Calldata address array must not be empty" |
||
| 646 |
); |
||
| 647 | |||
| 648 |
LocalVariables_OuterLiquidationFunction memory vars; |
||
| 649 |
LiquidationTotals memory totals; |
||
| 650 | |||
| 651 |
// taking fee to avoid accounted for the calculation of the TCR |
||
| 652 |
_syncGlobalAccounting(); |
||
| 653 | |||
| 654 |
vars.price = priceFeed.fetchPrice(); |
||
| 655 |
(uint256 _TCR, uint256 systemColl, uint256 systemDebt) = _getTCRWithSystemDebtAndCollShares( |
||
| 656 |
vars.price |
||
| 657 |
); |
||
| 658 |
vars.recoveryModeAtStart = _TCR < CCR ? true : false; |
||
| 659 | |||
| 660 |
// Perform the appropriate batch liquidation - tally values and obtain their totals. |
||
| 661 |
if (vars.recoveryModeAtStart) {
|
||
| 662 |
totals = _getTotalFromBatchLiquidate_RecoveryMode( |
||
| 663 |
vars.price, |
||
| 664 |
systemColl, |
||
| 665 |
systemDebt, |
||
| 666 |
_cdpArray |
||
| 667 |
); |
||
| 668 |
} else {
|
||
| 669 |
// if !vars.recoveryModeAtStart |
||
| 670 |
totals = _getTotalsFromBatchLiquidate_NormalMode(vars.price, _TCR, _cdpArray); |
||
| 671 |
} |
||
| 672 | |||
| 673 |
require(totals.totalDebtInSequence > 0, "LiquidationLibrary: nothing to liquidate"); |
||
| 674 | |||
| 675 |
// housekeeping leftover collateral for liquidated CDPs |
||
| 676 |
if (totals.totalCollSurplus > 0) {
|
||
| 677 |
activePool.transferSystemCollShares(address(collSurplusPool), totals.totalCollSurplus); |
||
| 678 |
} |
||
| 679 | |||
| 680 |
_finalizeLiquidation( |
||
| 681 |
totals.totalDebtToBurn, |
||
| 682 |
totals.totalCollToSendToLiquidator, |
||
| 683 |
totals.totalDebtToRedistribute, |
||
| 684 |
totals.totalCollReward, |
||
| 685 |
totals.totalCollSurplus, |
||
| 686 |
systemColl, |
||
| 687 |
systemDebt, |
||
| 688 |
vars.price |
||
| 689 |
); |
||
| 690 |
} |
||
| 691 | |||
| 692 |
/* |
||
| 693 |
* This function is used when the batch liquidation starts during Recovery Mode. However, it |
||
| 694 |
* handle the case where the system *leaves* Recovery Mode, part way through the liquidation processing |
||
| 695 |
*/ |
||
| 696 |
function _getTotalFromBatchLiquidate_RecoveryMode( |
||
| 697 |
uint256 _price, |
||
| 698 |
uint256 _systemCollShares, |
||
| 699 |
uint256 _systemDebt, |
||
| 700 |
bytes32[] memory _cdpArray |
||
| 701 |
) internal returns (LiquidationTotals memory totals) {
|
||
| 702 |
LocalVariables_LiquidationSequence memory vars; |
||
| 703 |
LiquidationValues memory singleLiquidation; |
||
| 704 | |||
| 705 |
vars.backToNormalMode = false; |
||
| 706 |
vars.entireSystemDebt = _systemDebt; |
||
| 707 |
vars.entireSystemColl = _systemCollShares; |
||
| 708 |
uint256 _TCR = _computeTCRWithGivenSystemValues( |
||
| 709 |
vars.entireSystemColl, |
||
| 710 |
vars.entireSystemDebt, |
||
| 711 |
_price |
||
| 712 |
); |
||
| 713 |
uint256 _cnt = _cdpArray.length; |
||
| 714 |
bool[] memory _liqFlags = new bool[](_cnt); |
||
| 715 |
uint256 _start; |
||
| 716 |
for (vars.i = _start; ; ) {
|
||
| 717 |
vars.cdpId = _cdpArray[vars.i]; |
||
| 718 |
// only for active cdps |
||
| 719 |
if (vars.cdpId != bytes32(0) && Cdps[vars.cdpId].status == Status.active) {
|
||
| 720 |
vars.ICR = getSyncedICR(vars.cdpId, _price); |
||
| 721 | |||
| 722 |
if ( |
||
| 723 |
!vars.backToNormalMode && |
||
| 724 |
(_checkICRAgainstMCR(vars.ICR) || canLiquidateRecoveryMode(vars.ICR, _TCR)) |
||
| 725 |
) {
|
||
| 726 |
vars.price = _price; |
||
| 727 |
_syncAccounting(vars.cdpId); |
||
| 728 |
_getLiquidationValuesRecoveryMode( |
||
| 729 |
_price, |
||
| 730 |
vars.entireSystemDebt, |
||
| 731 |
vars.entireSystemColl, |
||
| 732 |
vars, |
||
| 733 |
singleLiquidation |
||
| 734 |
); |
||
| 735 | |||
| 736 |
// Update aggregate trackers |
||
| 737 |
vars.entireSystemDebt = vars.entireSystemDebt - singleLiquidation.debtToBurn; |
||
| 738 |
vars.entireSystemColl = |
||
| 739 |
vars.entireSystemColl - |
||
| 740 |
singleLiquidation.totalCollToSendToLiquidator - |
||
| 741 |
singleLiquidation.collSurplus; |
||
| 742 | |||
| 743 |
// Add liquidation values to their respective running totals |
||
| 744 |
totals = _addLiquidationValuesToTotals(totals, singleLiquidation); |
||
| 745 | |||
| 746 |
_TCR = _computeTCRWithGivenSystemValues( |
||
| 747 |
vars.entireSystemColl, |
||
| 748 |
vars.entireSystemDebt, |
||
| 749 |
_price |
||
| 750 |
); |
||
| 751 |
vars.backToNormalMode = _TCR < CCR ? false : true; |
||
| 752 |
_liqFlags[vars.i] = true; |
||
| 753 |
} else if (vars.backToNormalMode && _checkICRAgainstMCR(vars.ICR)) {
|
||
| 754 |
_syncAccounting(vars.cdpId); |
||
| 755 |
_getLiquidationValuesNormalMode(_price, _TCR, vars, singleLiquidation); |
||
| 756 | |||
| 757 |
// Add liquidation values to their respective running totals |
||
| 758 |
totals = _addLiquidationValuesToTotals(totals, singleLiquidation); |
||
| 759 |
_liqFlags[vars.i] = true; |
||
| 760 |
} |
||
| 761 |
// In Normal Mode skip cdps with ICR >= MCR |
||
| 762 |
} |
||
| 763 |
++vars.i; |
||
| 764 |
if (vars.i == _cnt) {
|
||
| 765 |
break; |
||
| 766 |
} |
||
| 767 |
} |
||
| 768 |
} |
||
| 769 | |||
| 770 |
function _getTotalsFromBatchLiquidate_NormalMode( |
||
| 771 |
uint256 _price, |
||
| 772 |
uint256 _TCR, |
||
| 773 |
bytes32[] memory _cdpArray |
||
| 774 |
) internal returns (LiquidationTotals memory totals) {
|
||
| 775 |
LocalVariables_LiquidationSequence memory vars; |
||
| 776 |
LiquidationValues memory singleLiquidation; |
||
| 777 |
uint256 _cnt = _cdpArray.length; |
||
| 778 |
uint256 _start; |
||
| 779 |
for (vars.i = _start; ; ) {
|
||
| 780 |
vars.cdpId = _cdpArray[vars.i]; |
||
| 781 |
// only for active cdps |
||
| 782 |
if (vars.cdpId != bytes32(0) && Cdps[vars.cdpId].status == Status.active) {
|
||
| 783 |
vars.ICR = getSyncedICR(vars.cdpId, _price); |
||
| 784 | |||
| 785 |
if (_checkICRAgainstMCR(vars.ICR)) {
|
||
| 786 |
_syncAccounting(vars.cdpId); |
||
| 787 |
_getLiquidationValuesNormalMode(_price, _TCR, vars, singleLiquidation); |
||
| 788 | |||
| 789 |
// Add liquidation values to their respective running totals |
||
| 790 |
totals = _addLiquidationValuesToTotals(totals, singleLiquidation); |
||
| 791 |
} |
||
| 792 |
} |
||
| 793 |
++vars.i; |
||
| 794 |
if (vars.i == _cnt) {
|
||
| 795 |
break; |
||
| 796 |
} |
||
| 797 |
} |
||
| 798 |
} |
||
| 799 | |||
| 800 |
// --- Liquidation helper functions --- |
||
| 801 | |||
| 802 |
function _addLiquidationValuesToTotals( |
||
| 803 |
LiquidationTotals memory oldTotals, |
||
| 804 |
LiquidationValues memory singleLiquidation |
||
| 805 |
) internal pure returns (LiquidationTotals memory newTotals) {
|
||
| 806 |
// Tally all the values with their respective running totals |
||
| 807 |
newTotals.totalDebtInSequence = |
||
| 808 |
oldTotals.totalDebtInSequence + |
||
| 809 |
singleLiquidation.entireCdpDebt; |
||
| 810 |
newTotals.totalDebtToBurn = oldTotals.totalDebtToBurn + singleLiquidation.debtToBurn; |
||
| 811 |
newTotals.totalCollToSendToLiquidator = |
||
| 812 |
oldTotals.totalCollToSendToLiquidator + |
||
| 813 |
singleLiquidation.totalCollToSendToLiquidator; |
||
| 814 |
newTotals.totalDebtToRedistribute = |
||
| 815 |
oldTotals.totalDebtToRedistribute + |
||
| 816 |
singleLiquidation.debtToRedistribute; |
||
| 817 |
newTotals.totalCollSurplus = oldTotals.totalCollSurplus + singleLiquidation.collSurplus; |
||
| 818 |
newTotals.totalCollReward = |
||
| 819 |
oldTotals.totalCollReward + |
||
| 820 |
singleLiquidation.liquidatorCollSharesReward; |
||
| 821 | |||
| 822 |
return newTotals; |
||
| 823 |
} |
||
| 824 | |||
| 825 |
function _redistributeDebt(uint256 _debt) internal {
|
||
| 826 |
if (_debt == 0) {
|
||
| 827 |
return; |
||
| 828 |
} |
||
| 829 | |||
| 830 |
/* |
||
| 831 |
* Add distributed debt rewards-per-unit-staked to the running totals. Division uses a "feedback" |
||
| 832 |
* error correction, to keep the cumulative error low in the running totals systemDebtRedistributionIndex: |
||
| 833 |
* |
||
| 834 |
* 1) Form numerators which compensate for the floor division errors that occurred the last time this |
||
| 835 |
* function was called. |
||
| 836 |
* 2) Calculate "per-unit-staked" ratios. |
||
| 837 |
* 3) Multiply each ratio back by its denominator, to reveal the current floor division error. |
||
| 838 |
* 4) Store these errors for use in the next correction when this function is called. |
||
| 839 |
* 5) Note: static analysis tools complain about this "division before multiplication", however, it is intended. |
||
| 840 |
*/ |
||
| 841 |
uint256 EBTCDebtNumerator = (_debt * DECIMAL_PRECISION) + lastEBTCDebtErrorRedistribution; |
||
| 842 | |||
| 843 |
// Get the per-unit-staked terms |
||
| 844 |
uint256 _totalStakes = totalStakes; |
||
| 845 |
uint256 EBTCDebtRewardPerUnitStaked = EBTCDebtNumerator / _totalStakes; |
||
| 846 | |||
| 847 |
lastEBTCDebtErrorRedistribution = |
||
| 848 |
EBTCDebtNumerator - |
||
| 849 |
(EBTCDebtRewardPerUnitStaked * _totalStakes); |
||
| 850 | |||
| 851 |
// Add per-unit-staked terms to the running totals |
||
| 852 |
systemDebtRedistributionIndex = systemDebtRedistributionIndex + EBTCDebtRewardPerUnitStaked; |
||
| 853 | |||
| 854 |
emit SystemDebtRedistributionIndexUpdated(systemDebtRedistributionIndex); |
||
| 855 |
} |
||
| 856 | |||
| 857 |
// --- 'require' wrapper functions --- |
||
| 858 | |||
| 859 |
function _requirePartialLiqDebtSize( |
||
| 860 |
uint256 _partialDebt, |
||
| 861 |
uint256 _entireDebt, |
||
| 862 |
uint256 _price |
||
| 863 |
) internal view {
|
||
| 864 |
require( |
||
| 865 |
√
|
(_partialDebt + _convertDebtDenominationToBtc(MIN_NET_COLL, _price)) <= _entireDebt, |
|
| 866 |
"LiquidationLibrary: Partial debt liquidated must be less than total debt" |
||
| 867 |
); |
||
| 868 |
} |
||
| 869 | |||
| 870 |
function _requirePartialLiqCollSize(uint256 _entireColl) internal pure {
|
||
| 871 |
require( |
||
| 872 |
√
|
_entireColl >= MIN_NET_COLL, |
|
| 873 |
"LiquidationLibrary: Coll remaining in partially liquidated CDP must be >= minimum" |
||
| 874 |
); |
||
| 875 |
} |
||
| 876 |
} |
||
| 877 |
| Lines covered: | 28 / 39 (71.8%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Interfaces/IPriceFeed.sol"; |
||
| 6 |
import "./Interfaces/ICdpManager.sol"; |
||
| 7 |
import "./Interfaces/ISortedCdps.sol"; |
||
| 8 |
import "./Interfaces/ICdpManagerData.sol"; |
||
| 9 |
import "./Dependencies/EbtcBase.sol"; |
||
| 10 | |||
| 11 |
/// @notice Helper to turn a sequence into CDP id array for batch liquidation |
||
| 12 |
/// @dev Note this sequencer only serves as an approximation tool to provide "best-effort" |
||
| 13 |
/// @dev that return a list of CDP ids which could be consumed by "CdpManager.batchLiquidateCdps()". |
||
| 14 |
/// @dev It is possible that some of the returned CDPs might be skipped (not liquidatable any more) |
||
| 15 |
/// @dev during liquidation execution due to change of the system states |
||
| 16 |
/// @dev e.g., TCR brought back from Recovery Mode to Normal Mode |
||
| 17 |
contract LiquidationSequencer is EbtcBase {
|
||
| 18 |
ICdpManager public immutable cdpManager; |
||
| 19 |
ISortedCdps public immutable sortedCdps; |
||
| 20 | |||
| 21 |
constructor( |
||
| 22 |
address _cdpManagerAddress, |
||
| 23 |
address _sortedCdpsAddress, |
||
| 24 |
address _priceFeedAddress, |
||
| 25 |
address _activePoolAddress, |
||
| 26 |
address _collateralAddress |
||
| 27 |
√
|
) EbtcBase(_activePoolAddress, _priceFeedAddress, _collateralAddress) {
|
|
| 28 |
√
|
cdpManager = ICdpManager(_cdpManagerAddress); |
|
| 29 |
√
|
sortedCdps = ISortedCdps(_sortedCdpsAddress); |
|
| 30 |
} |
||
| 31 | |||
| 32 |
/// @dev Get first N batch of liquidatable Cdps at current price |
||
| 33 |
/// @dev Non-view function that updates and returns live price at execution time |
||
| 34 |
/// @dev could use callStatic offline to save gas |
||
| 35 |
function sequenceLiqToBatchLiq(uint256 _n) external returns (bytes32[] memory _array) {
|
||
| 36 |
uint256 _price = priceFeed.fetchPrice(); |
||
| 37 |
return sequenceLiqToBatchLiqWithPrice(_n, _price); |
||
| 38 |
} |
||
| 39 | |||
| 40 |
/// @dev Get first N batch of liquidatable Cdps at specified price |
||
| 41 |
/// @dev Non-view function that will sync global state |
||
| 42 |
/// @dev could use callStatic offline to save gas |
||
| 43 |
function sequenceLiqToBatchLiqWithPrice( |
||
| 44 |
uint256 _n, |
||
| 45 |
uint256 _price |
||
| 46 |
√
|
) public returns (bytes32[] memory _array) {
|
|
| 47 |
√
|
cdpManager.syncGlobalAccountingAndGracePeriod(); |
|
| 48 |
√
|
(uint256 _TCR, , ) = _getTCRWithSystemDebtAndCollShares(_price); |
|
| 49 |
√
|
return _sequenceLiqToBatchLiq(_n, _price, _TCR); |
|
| 50 |
} |
||
| 51 | |||
| 52 |
// return CdpId array (in NICR-decreasing order same as SortedCdps) |
||
| 53 |
// including the last N CDPs in sortedCdps for batch liquidation |
||
| 54 |
function _sequenceLiqToBatchLiq( |
||
| 55 |
uint256 _n, |
||
| 56 |
uint256 _price, |
||
| 57 |
uint256 _TCR |
||
| 58 |
√
|
) internal view returns (bytes32[] memory _array) {
|
|
| 59 |
√
|
if (_n > 0) {
|
|
| 60 |
// get count of liquidatable CDPs with 1st iteration |
||
| 61 |
√
|
(uint256 _cnt, ) = _iterateOverSortedCdps(0, _TCR, _n, _price); |
|
| 62 | |||
| 63 |
// retrieve liquidatable CDPs with 2nd iteration |
||
| 64 |
√
|
(uint256 _j, bytes32[] memory _returnedArray) = _iterateOverSortedCdps( |
|
| 65 |
√
|
_cnt, |
|
| 66 |
√
|
_TCR, |
|
| 67 |
√
|
_n, |
|
| 68 |
√
|
_price |
|
| 69 |
); |
||
| 70 |
√
|
require(_j == _cnt, "LiquidationSequencer: wrong sequence conversion!"); |
|
| 71 |
√
|
_array = _returnedArray; |
|
| 72 |
} |
||
| 73 |
} |
||
| 74 | |||
| 75 |
function _iterateOverSortedCdps( |
||
| 76 |
uint256 _realCount, |
||
| 77 |
uint256 _TCR, |
||
| 78 |
uint256 _n, |
||
| 79 |
uint256 _price |
||
| 80 |
√
|
) internal view returns (uint256 _cnt, bytes32[] memory _array) {
|
|
| 81 |
// if there is already a count (calculated from previous iteration) |
||
| 82 |
// we use the value to initialize CDP id array for return |
||
| 83 |
√
|
if (_realCount > 0) {
|
|
| 84 |
_array = new bytes32[](_realCount); |
||
| 85 |
} |
||
| 86 | |||
| 87 |
// initialize variables for this iteration |
||
| 88 |
√
|
bytes32 _last = sortedCdps.getLast(); |
|
| 89 |
√
|
bytes32 _first = sortedCdps.getFirst(); |
|
| 90 |
√
|
bytes32 _cdpId = _last; |
|
| 91 | |||
| 92 |
√
|
for (uint256 i = 0; i < (_realCount > 0 ? _realCount : _n) && _cdpId != _first; ) {
|
|
| 93 |
√
|
bool _liquidatable = _checkICRAgainstLiqThreshold( |
|
| 94 |
√
|
cdpManager.getSyncedICR(_cdpId, _price), |
|
| 95 |
√
|
_TCR |
|
| 96 |
); |
||
| 97 |
√
|
if (_liquidatable) {
|
|
| 98 |
if (_realCount > 0) {
|
||
| 99 |
_array[_realCount - _cnt - 1] = _cdpId; |
||
| 100 |
} |
||
| 101 |
unchecked {
|
||
| 102 |
++_cnt; |
||
| 103 |
} |
||
| 104 |
_cdpId = sortedCdps.getPrev(_cdpId); |
||
| 105 |
} else {
|
||
| 106 |
// breaking loop early if not liquidatable due to sorted (descending) list of CDPs |
||
| 107 |
√
|
break; |
|
| 108 |
} |
||
| 109 |
unchecked {
|
||
| 110 |
++i; |
||
| 111 |
} |
||
| 112 |
} |
||
| 113 |
} |
||
| 114 |
} |
||
| 115 |
| Lines covered: | 141 / 226 (62.4%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Interfaces/ISortedCdps.sol"; |
||
| 6 |
import "./Interfaces/ICdpManager.sol"; |
||
| 7 |
import "./Interfaces/IBorrowerOperations.sol"; |
||
| 8 | |||
| 9 |
/* |
||
| 10 |
* A sorted doubly linked list with nodes sorted in descending order. |
||
| 11 |
* |
||
| 12 |
* Nodes map to active Cdps in the system by ID. |
||
| 13 |
* Nodes are ordered according to their current nominal individual collateral ratio (NICR), |
||
| 14 |
* which is like the ICR but without the price, i.e., just collateral / debt. |
||
| 15 |
* |
||
| 16 |
* The list optionally accepts insert position hints. |
||
| 17 |
* |
||
| 18 |
* NICRs are computed dynamically at runtime, and not stored on the Node. This is because NICRs of active Cdps |
||
| 19 |
* change dynamically as liquidation events occur. |
||
| 20 |
* |
||
| 21 |
* The list relies on the fact that liquidation events preserve ordering: a liquidation decreases the NICRs of all active Cdps, |
||
| 22 |
* but maintains their order. A node inserted based on current NICR will maintain the correct position, |
||
| 23 |
* relative to it's peers, as rewards accumulate, as long as it's raw collateral and debt have not changed. |
||
| 24 |
* Thus, Nodes remain sorted by current NICR. |
||
| 25 |
* |
||
| 26 |
* Nodes need only be re-inserted upon a Cdp operation - when the owner adds or removes collateral or debt |
||
| 27 |
* to their position. |
||
| 28 |
* |
||
| 29 |
* The list is a modification of the following audited SortedDoublyLinkedList: |
||
| 30 |
* https://github.com/livepeer/protocol/blob/master/contracts/libraries/SortedDoublyLL.sol |
||
| 31 |
* |
||
| 32 |
* |
||
| 33 |
* Changes made in the Liquity implementation: |
||
| 34 |
* |
||
| 35 |
* - Keys have been removed from nodes |
||
| 36 |
* |
||
| 37 |
* - Ordering checks for insertion are performed by comparing an NICR argument to the current NICR, calculated at runtime. |
||
| 38 |
* The list relies on the property that ordering by ICR is maintained as the stETH:BTC price varies. |
||
| 39 |
* |
||
| 40 |
* - Public functions with parameters have been made internal to save gas, and given an external wrapper function for external access |
||
| 41 |
*/ |
||
| 42 |
contract SortedCdps is ISortedCdps {
|
||
| 43 |
string public constant NAME = "SortedCdps"; |
||
| 44 | |||
| 45 |
address public immutable borrowerOperationsAddress; |
||
| 46 | |||
| 47 |
ICdpManager public immutable cdpManager; |
||
| 48 | |||
| 49 |
uint256 public immutable maxSize; |
||
| 50 | |||
| 51 |
√
|
⟳
|
uint256 constant ADDRESS_SHIFT = 96; // 8 * 12; Puts the address at leftmost bytes32 position |
| 52 |
√
|
uint256 constant BLOCK_SHIFT = 64; // 8 * 8; Puts the block value after the address |
|
| 53 | |||
| 54 |
// Information for a node in the list |
||
| 55 |
struct Node {
|
||
| 56 |
bytes32 nextId; // Id of next node (smaller NICR) in the list |
||
| 57 |
bytes32 prevId; // Id of previous node (larger NICR) in the list |
||
| 58 |
} |
||
| 59 | |||
| 60 |
// Information for the list |
||
| 61 |
struct Data {
|
||
| 62 |
bytes32 head; // Head of the list. Also the node in the list with the largest NICR |
||
| 63 |
bytes32 tail; // Tail of the list. Also the node in the list with the smallest NICR |
||
| 64 |
uint256 size; // Current size of the list |
||
| 65 |
mapping(bytes32 => Node) nodes; // Track the corresponding ids for each node in the list |
||
| 66 |
} |
||
| 67 | |||
| 68 |
Data public data; |
||
| 69 | |||
| 70 |
uint256 public nextCdpNonce; |
||
| 71 |
bytes32 public constant dummyId = |
||
| 72 |
√
|
⟳
|
0x0000000000000000000000000000000000000000000000000000000000000000; |
| 73 | |||
| 74 |
// --- Dependency setters --- |
||
| 75 |
constructor(uint256 _size, address _cdpManagerAddress, address _borrowerOperationsAddress) {
|
||
| 76 |
√
|
if (_size == 0) {
|
|
| 77 |
_size = type(uint256).max; |
||
| 78 |
} |
||
| 79 | |||
| 80 |
√
|
maxSize = _size; |
|
| 81 | |||
| 82 |
√
|
cdpManager = ICdpManager(_cdpManagerAddress); |
|
| 83 |
√
|
borrowerOperationsAddress = _borrowerOperationsAddress; |
|
| 84 |
} |
||
| 85 | |||
| 86 |
// https://github.com/balancer-labs/balancer-v2-monorepo/blob/18bd5fb5d87b451cc27fbd30b276d1fb2987b529/pkg/vault/contracts/PoolRegistry.sol |
||
| 87 |
function toCdpId( |
||
| 88 |
address owner, |
||
| 89 |
uint256 blockHeight, |
||
| 90 |
uint256 nonce |
||
| 91 |
√
|
) public pure returns (bytes32) {
|
|
| 92 |
√
|
bytes32 serialized; |
|
| 93 | |||
| 94 |
√
|
serialized |= bytes32(nonce); |
|
| 95 |
√
|
serialized |= bytes32(blockHeight) << BLOCK_SHIFT; // to accommendate more than 4.2 billion blocks |
|
| 96 |
√
|
serialized |= bytes32(uint256(uint160(owner))) << ADDRESS_SHIFT; |
|
| 97 | |||
| 98 |
√
|
return serialized; |
|
| 99 |
} |
||
| 100 | |||
| 101 |
/// @notice Get owner address of a given Cdp, by CdpId. |
||
| 102 |
/// @dev The owner address is stored in the first 20 bytes of the CdpId |
||
| 103 |
/// @param cdpId cdpId of Cdp to get owner of |
||
| 104 |
/// @return owner address of the Cdp |
||
| 105 |
√
|
⟳
|
function getOwnerAddress(bytes32 cdpId) public pure override returns (address) {
|
| 106 |
√
|
⟳
|
uint256 _tmp = uint256(cdpId) >> ADDRESS_SHIFT; |
| 107 |
√
|
⟳
|
return address(uint160(_tmp)); |
| 108 |
} |
||
| 109 | |||
| 110 |
⟳
|
function nonExistId() public pure override returns (bytes32) {
|
|
| 111 |
⟳
|
return dummyId; |
|
| 112 |
} |
||
| 113 | |||
| 114 |
/// @notice Find a specific Cdp for a given owner, indexed by it's place in the linked list relative to other Cdps owned by the same address |
||
| 115 |
/// @notice Reverts if the index exceeds the number of active Cdps owned by the given owner |
||
| 116 |
/// @dev Intended for off-chain use, O(n) operation on size of SortedCdps linked list |
||
| 117 |
/// @param owner address of Cdp owner |
||
| 118 |
/// @param index index of Cdp, ordered by position in linked list relative to Cdps of the same owner |
||
| 119 |
function cdpOfOwnerByIndex( |
||
| 120 |
address owner, |
||
| 121 |
uint256 index |
||
| 122 |
√
|
⟳
|
) external view override returns (bytes32) {
|
| 123 |
√
|
⟳
|
(bytes32 _cdpId, ) = _cdpOfOwnerByIndex(owner, index, dummyId, 0); |
| 124 |
√
|
⟳
|
return _cdpId; |
| 125 |
} |
||
| 126 | |||
| 127 |
/// @dev a pagination-flavor search (from least ICR to biggest ICR) for CDP owned by given owner and specified index (starting at given CDP) |
||
| 128 |
/// @param startNodeId the seach traversal will start at this given CDP instead of the tail of the list |
||
| 129 |
/// @param maxNodes the traversal will go through the list by this given maximum limit of number of CDPs |
||
| 130 |
function cdpOfOwnerByIdx( |
||
| 131 |
address owner, |
||
| 132 |
uint256 index, |
||
| 133 |
bytes32 startNodeId, |
||
| 134 |
uint maxNodes |
||
| 135 |
) external view override returns (bytes32, bool) {
|
||
| 136 |
return _cdpOfOwnerByIndex(owner, index, startNodeId, maxNodes); |
||
| 137 |
} |
||
| 138 | |||
| 139 |
/// @dev return EITHER the found CDP owned by given owner & index with a true indicator OR |
||
| 140 |
/// @dev current lastly-visited CDP as the startNode for next pagination with a false indicator |
||
| 141 |
function _cdpOfOwnerByIndex( |
||
| 142 |
address owner, |
||
| 143 |
uint256 index, |
||
| 144 |
bytes32 startNodeId, |
||
| 145 |
uint maxNodes |
||
| 146 |
√
|
⟳
|
) internal view returns (bytes32, bool) {
|
| 147 |
// walk the list, until we get to the indexed CDP |
||
| 148 |
// start at the given node or from the tail of list |
||
| 149 |
√
|
⟳
|
bytes32 _currentCdpId = (startNodeId == dummyId ? data.tail : startNodeId); |
| 150 |
√
|
⟳
|
uint _currentIndex = 0; |
| 151 |
√
|
⟳
|
uint i; |
| 152 | |||
| 153 |
√
|
⟳
|
while (_currentCdpId != dummyId) {
|
| 154 |
// if the current Cdp is owned by specified owner |
||
| 155 |
√
|
⟳
|
if (getOwnerAddress(_currentCdpId) == owner) {
|
| 156 |
// if the current index of the owner Cdp matches specified index |
||
| 157 |
√
|
⟳
|
if (_currentIndex == index) {
|
| 158 |
√
|
⟳
|
return (_currentCdpId, true); |
| 159 |
} else {
|
||
| 160 |
// if not, increment the owner index as we've seen a Cdp owned by them |
||
| 161 |
_currentIndex = _currentIndex + 1; |
||
| 162 |
} |
||
| 163 |
} |
||
| 164 |
√
|
⟳
|
++i; |
| 165 | |||
| 166 |
// move to the next Cdp in the list |
||
| 167 |
√
|
⟳
|
_currentCdpId = data.nodes[_currentCdpId].prevId; |
| 168 | |||
| 169 |
// cut the run if we exceed expected iterations through the loop |
||
| 170 |
√
|
⟳
|
if (maxNodes > 0 && i >= maxNodes) {
|
| 171 |
break; |
||
| 172 |
} |
||
| 173 |
} |
||
| 174 |
// if we reach maximum iteration or end of list |
||
| 175 |
// without seeing the specified index for the owner |
||
| 176 |
// then maybe a new pagination is needed |
||
| 177 |
return (_currentCdpId, false); |
||
| 178 |
} |
||
| 179 | |||
| 180 |
/// @notice Get active Cdp count of a given address |
||
| 181 |
/// @dev Intended for off-chain use, O(n) operation on size of linked list |
||
| 182 |
√
|
⟳
|
function cdpCountOf(address owner) external view override returns (uint256) {
|
| 183 |
√
|
⟳
|
(uint256 _cnt, ) = _cdpCountOf(owner, dummyId, 0); |
| 184 |
√
|
⟳
|
return _cnt; |
| 185 |
} |
||
| 186 | |||
| 187 |
/// @dev a pagination-flavor search count of (from least ICR to biggest ICR) CDPs owned by given owner (starting at given CDP) |
||
| 188 |
/// @param startNodeId the count traversal will start at this given CDP instead of the tail of the list |
||
| 189 |
/// @param maxNodes the traversal will go through the list by this given maximum limit of number of CDPs |
||
| 190 |
function getCdpCountOf( |
||
| 191 |
address owner, |
||
| 192 |
bytes32 startNodeId, |
||
| 193 |
uint maxNodes |
||
| 194 |
) external view override returns (uint256, bytes32) {
|
||
| 195 |
return _cdpCountOf(owner, startNodeId, maxNodes); |
||
| 196 |
} |
||
| 197 | |||
| 198 |
/// @dev return the found CDP count owned by given owner with |
||
| 199 |
/// @dev current lastly-visited CDP as the startNode for next pagination |
||
| 200 |
function _cdpCountOf( |
||
| 201 |
address owner, |
||
| 202 |
bytes32 startNodeId, |
||
| 203 |
uint maxNodes |
||
| 204 |
√
|
⟳
|
) internal view returns (uint256, bytes32) {
|
| 205 |
// walk the list, until we get to the count |
||
| 206 |
// start at the given node or from the tail of list |
||
| 207 |
√
|
⟳
|
bytes32 _currentCdpId = (startNodeId == dummyId ? data.tail : startNodeId); |
| 208 |
√
|
⟳
|
uint _ownedCount = 0; |
| 209 |
√
|
⟳
|
uint i = 0; |
| 210 | |||
| 211 |
√
|
⟳
|
while (_currentCdpId != dummyId) {
|
| 212 |
// if the current Cdp is owned by specified owner |
||
| 213 |
√
|
⟳
|
if (getOwnerAddress(_currentCdpId) == owner) {
|
| 214 |
√
|
⟳
|
_ownedCount = _ownedCount + 1; |
| 215 |
} |
||
| 216 |
√
|
⟳
|
++i; |
| 217 | |||
| 218 |
// move to the next Cdp in the list |
||
| 219 |
√
|
⟳
|
_currentCdpId = data.nodes[_currentCdpId].prevId; |
| 220 | |||
| 221 |
// cut the run if we exceed expected iterations through the loop |
||
| 222 |
√
|
⟳
|
if (maxNodes > 0 && i >= maxNodes) {
|
| 223 |
break; |
||
| 224 |
} |
||
| 225 |
} |
||
| 226 |
√
|
⟳
|
return (_ownedCount, _currentCdpId); |
| 227 |
} |
||
| 228 | |||
| 229 |
/// @notice Get all active Cdps for a given address |
||
| 230 |
/// @dev Intended for off-chain use, O(n) operation on size of linked list |
||
| 231 |
function getCdpsOf(address owner) external view override returns (bytes32[] memory cdps) {
|
||
| 232 |
// Naive method uses two-pass strategy to determine exactly how many Cdps are owned by owner |
||
| 233 |
// This roughly halves the amount of Cdps we can process before relying on pagination or off-chain methods |
||
| 234 |
(uint _ownedCount, ) = _cdpCountOf(owner, dummyId, 0); |
||
| 235 |
if (_ownedCount > 0) {
|
||
| 236 |
(bytes32[] memory _allCdps, , ) = _getCdpsOf(owner, dummyId, 0, _ownedCount); |
||
| 237 |
cdps = _allCdps; |
||
| 238 |
} |
||
| 239 |
} |
||
| 240 | |||
| 241 |
/// @dev a pagination-flavor search retrieval of (from least ICR to biggest ICR) CDPs owned by given owner (starting at given CDP) |
||
| 242 |
/// @param startNodeId the traversal will start at this given CDP instead of the tail of the list |
||
| 243 |
/// @param maxNodes the traversal will go through the list by this given maximum limit of number of CDPs |
||
| 244 |
function getAllCdpsOf( |
||
| 245 |
address owner, |
||
| 246 |
bytes32 startNodeId, |
||
| 247 |
uint maxNodes |
||
| 248 |
) external view override returns (bytes32[] memory, uint256, bytes32) {
|
||
| 249 |
// Naive method uses two-pass strategy to determine exactly how many Cdps are owned by owner |
||
| 250 |
// This roughly halves the amount of Cdps we can process before relying on pagination or off-chain methods |
||
| 251 |
(uint _ownedCount, ) = _cdpCountOf(owner, startNodeId, maxNodes); |
||
| 252 |
return _getCdpsOf(owner, startNodeId, maxNodes, _ownedCount); |
||
| 253 |
} |
||
| 254 | |||
| 255 |
/// @dev return EITHER the found CDPs (also the count) owned by given owner OR empty array with |
||
| 256 |
/// @dev current lastly-visited CDP as the startNode for next pagination |
||
| 257 |
function _getCdpsOf( |
||
| 258 |
address owner, |
||
| 259 |
bytes32 startNodeId, |
||
| 260 |
uint maxNodes, |
||
| 261 |
uint maxArraySize |
||
| 262 |
) internal view returns (bytes32[] memory, uint256, bytes32) {
|
||
| 263 |
if (maxArraySize == 0) {
|
||
| 264 |
return (new bytes32[](0), 0, dummyId); |
||
| 265 |
} |
||
| 266 | |||
| 267 |
// Two-pass strategy, halving the amount of Cdps we can process before relying on pagination or off-chain methods |
||
| 268 |
bytes32[] memory userCdps = new bytes32[](maxArraySize); |
||
| 269 |
uint i = 0; |
||
| 270 |
uint _cdpRetrieved; |
||
| 271 | |||
| 272 |
// walk the list, until we get to the index |
||
| 273 |
// start at the given node or from the tail of list |
||
| 274 |
bytes32 _currentCdpId = (startNodeId == dummyId ? data.tail : startNodeId); |
||
| 275 | |||
| 276 |
while (_currentCdpId != dummyId) {
|
||
| 277 |
// if the current Cdp is owned by specified owner |
||
| 278 |
if (getOwnerAddress(_currentCdpId) == owner) {
|
||
| 279 |
userCdps[_cdpRetrieved] = _currentCdpId; |
||
| 280 |
++_cdpRetrieved; |
||
| 281 |
} |
||
| 282 |
++i; |
||
| 283 | |||
| 284 |
// move to the next Cdp in the list |
||
| 285 |
_currentCdpId = data.nodes[_currentCdpId].prevId; |
||
| 286 | |||
| 287 |
// cut the run if we exceed expected iterations through the loop |
||
| 288 |
if (maxNodes > 0 && i >= maxNodes) {
|
||
| 289 |
break; |
||
| 290 |
} |
||
| 291 |
} |
||
| 292 | |||
| 293 |
return (userCdps, _cdpRetrieved, _currentCdpId); |
||
| 294 |
} |
||
| 295 | |||
| 296 |
/* |
||
| 297 |
* @dev Add a node to the list |
||
| 298 |
* @param owner cdp owner |
||
| 299 |
* @param _NICR Node's NICR |
||
| 300 |
* @param _prevId Id of previous node for the insert position |
||
| 301 |
* @param _nextId Id of next node for the insert position |
||
| 302 |
*/ |
||
| 303 |
function insert( |
||
| 304 |
address owner, |
||
| 305 |
uint256 _NICR, |
||
| 306 |
bytes32 _prevId, |
||
| 307 |
bytes32 _nextId |
||
| 308 |
√
|
) external override returns (bytes32) {
|
|
| 309 |
√
|
_requireCallerIsBOorCdpM(); |
|
| 310 |
√
|
bytes32 _id = toCdpId(owner, block.number, nextCdpNonce); |
|
| 311 |
√
|
require(cdpManager.getCdpStatus(_id) == 0, "SortedCdps: new id is NOT nonExistent!"); |
|
| 312 | |||
| 313 |
√
|
_insert(_id, _NICR, _prevId, _nextId); |
|
| 314 | |||
| 315 |
unchecked {
|
||
| 316 |
√
|
++nextCdpNonce; |
|
| 317 |
} |
||
| 318 | |||
| 319 |
√
|
return _id; |
|
| 320 |
} |
||
| 321 | |||
| 322 |
function _insert(bytes32 _id, uint256 _NICR, bytes32 _prevId, bytes32 _nextId) internal {
|
||
| 323 |
// List must not be full |
||
| 324 |
√
|
require(!isFull(), "SortedCdps: List is full"); |
|
| 325 |
// List must not already contain node |
||
| 326 |
√
|
require(!contains(_id), "SortedCdps: List already contains the node"); |
|
| 327 |
// Node id must not be null |
||
| 328 |
√
|
require(_id != dummyId, "SortedCdps: Id cannot be zero"); |
|
| 329 |
// NICR must be non-zero |
||
| 330 |
√
|
require(_NICR > 0, "SortedCdps: NICR must be positive"); |
|
| 331 | |||
| 332 |
√
|
bytes32 prevId = _prevId; |
|
| 333 |
√
|
bytes32 nextId = _nextId; |
|
| 334 | |||
| 335 |
√
|
if (!_validInsertPosition(_NICR, prevId, nextId)) {
|
|
| 336 |
// Sender's hint was not a valid insert position |
||
| 337 |
// Use sender's hint to find a valid insert position |
||
| 338 |
√
|
(prevId, nextId) = _findInsertPosition(_NICR, prevId, nextId); |
|
| 339 |
} |
||
| 340 | |||
| 341 |
√
|
if (prevId == dummyId && nextId == dummyId) {
|
|
| 342 |
// Insert as head and tail |
||
| 343 |
√
|
data.head = _id; |
|
| 344 |
√
|
data.tail = _id; |
|
| 345 |
√
|
} else if (prevId == dummyId) {
|
|
| 346 |
// Insert before `prevId` as the head |
||
| 347 |
√
|
data.nodes[_id].nextId = data.head; |
|
| 348 |
√
|
data.nodes[data.head].prevId = _id; |
|
| 349 |
√
|
data.head = _id; |
|
| 350 |
√
|
} else if (nextId == dummyId) {
|
|
| 351 |
// Insert after `nextId` as the tail |
||
| 352 |
√
|
data.nodes[_id].prevId = data.tail; |
|
| 353 |
√
|
data.nodes[data.tail].nextId = _id; |
|
| 354 |
√
|
data.tail = _id; |
|
| 355 |
} else {
|
||
| 356 |
// Insert at insert position between `prevId` and `nextId` |
||
| 357 |
data.nodes[_id].nextId = nextId; |
||
| 358 |
data.nodes[_id].prevId = prevId; |
||
| 359 |
data.nodes[prevId].nextId = _id; |
||
| 360 |
data.nodes[nextId].prevId = _id; |
||
| 361 |
} |
||
| 362 | |||
| 363 |
√
|
data.size = data.size + 1; |
|
| 364 |
√
|
emit NodeAdded(_id, _NICR); |
|
| 365 |
} |
||
| 366 | |||
| 367 |
function remove(bytes32 _id) external override {
|
||
| 368 |
⟳
|
_requireCallerIsCdpManager(); |
|
| 369 |
⟳
|
_remove(_id); |
|
| 370 |
} |
||
| 371 | |||
| 372 |
function batchRemove(bytes32[] memory _ids) external override {
|
||
| 373 |
_requireCallerIsCdpManager(); |
||
| 374 |
uint256 _len = _ids.length; |
||
| 375 |
require(_len > 1, "SortedCdps: batchRemove() only apply to multiple cdpIds!"); |
||
| 376 | |||
| 377 |
bytes32 _firstPrev = data.nodes[_ids[0]].prevId; |
||
| 378 |
bytes32 _lastNext = data.nodes[_ids[_len - 1]].nextId; |
||
| 379 | |||
| 380 |
require( |
||
| 381 |
_firstPrev != dummyId || _lastNext != dummyId, |
||
| 382 |
"SortedCdps: batchRemove() leave ZERO node left!" |
||
| 383 |
); |
||
| 384 | |||
| 385 |
for (uint256 i = 0; i < _len; ++i) {
|
||
| 386 |
require(contains(_ids[i]), "SortedCdps: List does not contain the id"); |
||
| 387 |
} |
||
| 388 | |||
| 389 |
// orphan nodes in between to save gas |
||
| 390 |
if (_firstPrev != dummyId) {
|
||
| 391 |
data.nodes[_firstPrev].nextId = _lastNext; |
||
| 392 |
} else {
|
||
| 393 |
data.head = _lastNext; |
||
| 394 |
} |
||
| 395 |
if (_lastNext != dummyId) {
|
||
| 396 |
data.nodes[_lastNext].prevId = _firstPrev; |
||
| 397 |
} else {
|
||
| 398 |
data.tail = _firstPrev; |
||
| 399 |
} |
||
| 400 | |||
| 401 |
// delete node & owner storages to get gas refund |
||
| 402 |
for (uint i = 0; i < _len; ++i) {
|
||
| 403 |
delete data.nodes[_ids[i]]; |
||
| 404 |
emit NodeRemoved(_ids[i]); |
||
| 405 |
} |
||
| 406 |
data.size = data.size - _len; |
||
| 407 |
} |
||
| 408 | |||
| 409 |
/* |
||
| 410 |
* @dev Remove a node from the list |
||
| 411 |
* @param _id Node's id |
||
| 412 |
*/ |
||
| 413 |
function _remove(bytes32 _id) internal {
|
||
| 414 |
// List must contain the node |
||
| 415 |
√
|
⟳
|
require(contains(_id), "SortedCdps: List does not contain the id"); |
| 416 | |||
| 417 |
√
|
⟳
|
if (data.size > 1) {
|
| 418 |
// List contains more than a single node |
||
| 419 |
√
|
⟳
|
if (_id == data.head) {
|
| 420 |
// The removed node is the head |
||
| 421 |
// Set head to next node |
||
| 422 |
√
|
⟳
|
data.head = data.nodes[_id].nextId; |
| 423 |
// Set prev pointer of new head to null |
||
| 424 |
√
|
⟳
|
data.nodes[data.head].prevId = dummyId; |
| 425 |
√
|
} else if (_id == data.tail) {
|
|
| 426 |
// The removed node is the tail |
||
| 427 |
// Set tail to previous node |
||
| 428 |
√
|
data.tail = data.nodes[_id].prevId; |
|
| 429 |
// Set next pointer of new tail to null |
||
| 430 |
√
|
data.nodes[data.tail].nextId = dummyId; |
|
| 431 |
} else {
|
||
| 432 |
// The removed node is neither the head nor the tail |
||
| 433 |
// Set next pointer of previous node to the next node |
||
| 434 |
data.nodes[data.nodes[_id].prevId].nextId = data.nodes[_id].nextId; |
||
| 435 |
// Set prev pointer of next node to the previous node |
||
| 436 |
data.nodes[data.nodes[_id].nextId].prevId = data.nodes[_id].prevId; |
||
| 437 |
} |
||
| 438 |
} else {
|
||
| 439 |
// List contains a single node |
||
| 440 |
// Set the head and tail to null |
||
| 441 |
√
|
data.head = dummyId; |
|
| 442 |
√
|
data.tail = dummyId; |
|
| 443 |
} |
||
| 444 | |||
| 445 |
√
|
⟳
|
delete data.nodes[_id]; |
| 446 |
√
|
⟳
|
data.size = data.size - 1; |
| 447 |
√
|
⟳
|
emit NodeRemoved(_id); |
| 448 |
} |
||
| 449 | |||
| 450 |
/* |
||
| 451 |
* @dev Re-insert the node at a new position, based on its new NICR |
||
| 452 |
* @param _id Node's id |
||
| 453 |
* @param _newNICR Node's new NICR |
||
| 454 |
* @param _prevId Id of previous node for the new insert position |
||
| 455 |
* @param _nextId Id of next node for the new insert position |
||
| 456 |
*/ |
||
| 457 |
function reInsert( |
||
| 458 |
bytes32 _id, |
||
| 459 |
uint256 _newNICR, |
||
| 460 |
bytes32 _prevId, |
||
| 461 |
bytes32 _nextId |
||
| 462 |
) external override {
|
||
| 463 |
√
|
_requireCallerIsBOorCdpM(); |
|
| 464 |
// List must contain the node |
||
| 465 |
√
|
require(contains(_id), "SortedCdps: List does not contain the id"); |
|
| 466 |
// NICR must be non-zero |
||
| 467 |
√
|
require(_newNICR > 0, "SortedCdps: NICR must be positive"); |
|
| 468 | |||
| 469 |
// Remove node from the list |
||
| 470 |
√
|
_remove(_id); |
|
| 471 | |||
| 472 |
√
|
_insert(_id, _newNICR, _prevId, _nextId); |
|
| 473 |
} |
||
| 474 | |||
| 475 |
/** |
||
| 476 |
* @dev Checks if the list contains a given node |
||
| 477 |
* @param _id The ID of the node |
||
| 478 |
* @return true if the node exists, false otherwise |
||
| 479 |
*/ |
||
| 480 |
√
|
⟳
|
function contains(bytes32 _id) public view override returns (bool) {
|
| 481 |
√
|
⟳
|
bool _exist = _id != dummyId && (data.head == _id || data.tail == _id); |
| 482 |
√
|
⟳
|
if (!_exist) {
|
| 483 |
√
|
Node memory _node = data.nodes[_id]; |
|
| 484 |
√
|
_exist = _id != dummyId && (_node.nextId != dummyId && _node.prevId != dummyId); |
|
| 485 |
} |
||
| 486 |
√
|
⟳
|
return _exist; |
| 487 |
} |
||
| 488 | |||
| 489 |
/** |
||
| 490 |
* @dev Checks if the list is full |
||
| 491 |
* @return true if the list is full, false otherwise |
||
| 492 |
*/ |
||
| 493 |
√
|
function isFull() public view override returns (bool) {
|
|
| 494 |
√
|
return data.size == maxSize; |
|
| 495 |
} |
||
| 496 | |||
| 497 |
/** |
||
| 498 |
* @dev Checks if the list is empty |
||
| 499 |
* @return true if the list is empty, false otherwise |
||
| 500 |
*/ |
||
| 501 |
√
|
function isEmpty() public view override returns (bool) {
|
|
| 502 |
√
|
return data.size == 0; |
|
| 503 |
} |
||
| 504 | |||
| 505 |
/** |
||
| 506 |
* @dev Returns the current size of the list |
||
| 507 |
* @return The current size of the list |
||
| 508 |
*/ |
||
| 509 |
√
|
⟳
|
function getSize() external view override returns (uint256) {
|
| 510 |
√
|
⟳
|
return data.size; |
| 511 |
} |
||
| 512 | |||
| 513 |
/** |
||
| 514 |
* @dev Returns the maximum size of the list |
||
| 515 |
* @return The maximum size of the list |
||
| 516 |
*/ |
||
| 517 |
function getMaxSize() external view override returns (uint256) {
|
||
| 518 |
return maxSize; |
||
| 519 |
} |
||
| 520 | |||
| 521 |
/** |
||
| 522 |
* @dev Returns the first node in the list (node with the largest NICR) |
||
| 523 |
* @return The ID of the first node |
||
| 524 |
*/ |
||
| 525 |
√
|
⟳
|
function getFirst() external view override returns (bytes32) {
|
| 526 |
√
|
⟳
|
return data.head; |
| 527 |
} |
||
| 528 | |||
| 529 |
/** |
||
| 530 |
* @dev Returns the last node in the list (node with the smallest NICR) |
||
| 531 |
* @return The ID of the last node |
||
| 532 |
*/ |
||
| 533 |
√
|
⟳
|
function getLast() external view override returns (bytes32) {
|
| 534 |
√
|
⟳
|
return data.tail; |
| 535 |
} |
||
| 536 | |||
| 537 |
/** |
||
| 538 |
* @dev Returns the next node (with a smaller NICR) in the list for a given node |
||
| 539 |
* @param _id The ID of the node |
||
| 540 |
* @return The ID of the next node |
||
| 541 |
*/ |
||
| 542 |
√
|
⟳
|
function getNext(bytes32 _id) external view override returns (bytes32) {
|
| 543 |
√
|
⟳
|
return data.nodes[_id].nextId; |
| 544 |
} |
||
| 545 | |||
| 546 |
/** |
||
| 547 |
* @dev Returns the previous node (with a larger NICR) in the list for a given node |
||
| 548 |
* @param _id The ID of the node |
||
| 549 |
* @return The ID of the previous node |
||
| 550 |
*/ |
||
| 551 |
√
|
⟳
|
function getPrev(bytes32 _id) external view override returns (bytes32) {
|
| 552 |
√
|
⟳
|
return data.nodes[_id].prevId; |
| 553 |
} |
||
| 554 | |||
| 555 |
/* |
||
| 556 |
* @dev Check if a pair of nodes is a valid insertion point for a new node with the given NICR |
||
| 557 |
* @param _NICR Node's NICR |
||
| 558 |
* @param _prevId Id of previous node for the insert position |
||
| 559 |
* @param _nextId Id of next node for the insert position |
||
| 560 |
* @return true if the position is valid, false otherwise |
||
| 561 |
*/ |
||
| 562 |
function validInsertPosition( |
||
| 563 |
uint256 _NICR, |
||
| 564 |
bytes32 _prevId, |
||
| 565 |
bytes32 _nextId |
||
| 566 |
) external view override returns (bool) {
|
||
| 567 |
return _validInsertPosition(_NICR, _prevId, _nextId); |
||
| 568 |
} |
||
| 569 | |||
| 570 |
function _validInsertPosition( |
||
| 571 |
uint256 _NICR, |
||
| 572 |
bytes32 _prevId, |
||
| 573 |
bytes32 _nextId |
||
| 574 |
√
|
) internal view returns (bool) {
|
|
| 575 |
√
|
if (_prevId == dummyId && _nextId == dummyId) {
|
|
| 576 |
// `(null, null)` is a valid insert position if the list is empty |
||
| 577 |
√
|
return isEmpty(); |
|
| 578 |
√
|
} else if (_prevId == dummyId) {
|
|
| 579 |
// `(null, _nextId)` is a valid insert position if `_nextId` is the head of the list |
||
| 580 |
return data.head == _nextId && _NICR >= cdpManager.getNominalICR(_nextId); |
||
| 581 |
√
|
} else if (_nextId == dummyId) {
|
|
| 582 |
// `(_prevId, null)` is a valid insert position if `_prevId` is the tail of the list |
||
| 583 |
√
|
return data.tail == _prevId && _NICR <= cdpManager.getNominalICR(_prevId); |
|
| 584 |
} else {
|
||
| 585 |
// `(_prevId, _nextId)` is a valid insert position if they are adjacent nodes and `_NICR` falls between the two nodes' NICRs |
||
| 586 |
return |
||
| 587 |
√
|
data.nodes[_prevId].nextId == _nextId && |
|
| 588 |
cdpManager.getNominalICR(_prevId) >= _NICR && |
||
| 589 |
_NICR >= cdpManager.getNominalICR(_nextId); |
||
| 590 |
} |
||
| 591 |
} |
||
| 592 | |||
| 593 |
/* |
||
| 594 |
* @dev Descend the list (larger NICRs to smaller NICRs) to find a valid insert position |
||
| 595 |
* @param _NICR Node's NICR |
||
| 596 |
* @param _startId Id of node to start descending the list from |
||
| 597 |
*/ |
||
| 598 |
√
|
function _descendList(uint256 _NICR, bytes32 _startId) internal view returns (bytes32, bytes32) {
|
|
| 599 |
// If `_startId` is the head, check if the insert position is before the head |
||
| 600 |
√
|
if (data.head == _startId && _NICR >= cdpManager.getNominalICR(_startId)) {
|
|
| 601 |
√
|
return (dummyId, _startId); |
|
| 602 |
} |
||
| 603 | |||
| 604 |
√
|
bytes32 prevId = _startId; |
|
| 605 |
√
|
bytes32 nextId = data.nodes[prevId].nextId; |
|
| 606 | |||
| 607 |
// Descend the list until we reach the end or until we find a valid insert position |
||
| 608 |
√
|
while (prevId != dummyId && !_validInsertPosition(_NICR, prevId, nextId)) {
|
|
| 609 |
prevId = data.nodes[prevId].nextId; |
||
| 610 |
nextId = data.nodes[prevId].nextId; |
||
| 611 |
} |
||
| 612 | |||
| 613 |
√
|
return (prevId, nextId); |
|
| 614 |
} |
||
| 615 | |||
| 616 |
/* |
||
| 617 |
* @dev Ascend the list (smaller NICRs to larger NICRs) to find a valid insert position |
||
| 618 |
* @param _NICR Node's NICR |
||
| 619 |
* @param _startId Id of node to start ascending the list from |
||
| 620 |
*/ |
||
| 621 |
function _ascendList(uint256 _NICR, bytes32 _startId) internal view returns (bytes32, bytes32) {
|
||
| 622 |
// If `_startId` is the tail, check if the insert position is after the tail |
||
| 623 |
if (data.tail == _startId && _NICR <= cdpManager.getNominalICR(_startId)) {
|
||
| 624 |
return (_startId, dummyId); |
||
| 625 |
} |
||
| 626 | |||
| 627 |
bytes32 nextId = _startId; |
||
| 628 |
bytes32 prevId = data.nodes[nextId].prevId; |
||
| 629 | |||
| 630 |
// Ascend the list until we reach the end or until we find a valid insertion point |
||
| 631 |
while (nextId != dummyId && !_validInsertPosition(_NICR, prevId, nextId)) {
|
||
| 632 |
nextId = data.nodes[nextId].prevId; |
||
| 633 |
prevId = data.nodes[nextId].prevId; |
||
| 634 |
} |
||
| 635 | |||
| 636 |
return (prevId, nextId); |
||
| 637 |
} |
||
| 638 | |||
| 639 |
/* |
||
| 640 |
* @dev Find the insert position for a new node with the given NICR |
||
| 641 |
* @param _NICR Node's NICR |
||
| 642 |
* @param _prevId Id of previous node for the insert position |
||
| 643 |
* @param _nextId Id of next node for the insert position |
||
| 644 |
* @return The IDs of the previous and next nodes for the insert position |
||
| 645 |
*/ |
||
| 646 |
function findInsertPosition( |
||
| 647 |
uint256 _NICR, |
||
| 648 |
bytes32 _prevId, |
||
| 649 |
bytes32 _nextId |
||
| 650 |
) external view override returns (bytes32, bytes32) {
|
||
| 651 |
return _findInsertPosition(_NICR, _prevId, _nextId); |
||
| 652 |
} |
||
| 653 | |||
| 654 |
function _findInsertPosition( |
||
| 655 |
uint256 _NICR, |
||
| 656 |
bytes32 _prevId, |
||
| 657 |
bytes32 _nextId |
||
| 658 |
√
|
) internal view returns (bytes32, bytes32) {
|
|
| 659 |
√
|
bytes32 prevId = _prevId; |
|
| 660 |
√
|
bytes32 nextId = _nextId; |
|
| 661 | |||
| 662 |
√
|
if (prevId != dummyId) {
|
|
| 663 |
√
|
if (!contains(prevId) || _NICR > cdpManager.getNominalICR(prevId)) {
|
|
| 664 |
// `prevId` does not exist anymore or now has a smaller NICR than the given NICR |
||
| 665 |
√
|
prevId = dummyId; |
|
| 666 |
} |
||
| 667 |
} |
||
| 668 | |||
| 669 |
√
|
if (nextId != dummyId) {
|
|
| 670 |
√
|
if (!contains(nextId) || _NICR < cdpManager.getNominalICR(nextId)) {
|
|
| 671 |
// `nextId` does not exist anymore or now has a larger NICR than the given NICR |
||
| 672 |
√
|
nextId = dummyId; |
|
| 673 |
} |
||
| 674 |
} |
||
| 675 | |||
| 676 |
√
|
if (prevId == dummyId && nextId == dummyId) {
|
|
| 677 |
// No hint - descend list starting from head |
||
| 678 |
√
|
return _descendList(_NICR, data.head); |
|
| 679 |
} else if (prevId == dummyId) {
|
||
| 680 |
// No `prevId` for hint - ascend list starting from `nextId` |
||
| 681 |
return _ascendList(_NICR, nextId); |
||
| 682 |
} else if (nextId == dummyId) {
|
||
| 683 |
// No `nextId` for hint - descend list starting from `prevId` |
||
| 684 |
return _descendList(_NICR, prevId); |
||
| 685 |
} else {
|
||
| 686 |
// Descend list starting from `prevId` |
||
| 687 |
return _descendList(_NICR, prevId); |
||
| 688 |
} |
||
| 689 |
} |
||
| 690 | |||
| 691 |
// --- 'require' functions --- |
||
| 692 | |||
| 693 |
/// @dev Asserts that the caller of the function is the CdpManager |
||
| 694 |
function _requireCallerIsCdpManager() internal view {
|
||
| 695 |
⟳
|
require(msg.sender == address(cdpManager), "SortedCdps: Caller is not the CdpManager"); |
|
| 696 |
} |
||
| 697 | |||
| 698 |
/// @dev Asserts that the caller of the function is either the BorrowerOperations contract or the CdpManager |
||
| 699 |
function _requireCallerIsBOorCdpM() internal view {
|
||
| 700 |
require( |
||
| 701 |
√
|
msg.sender == borrowerOperationsAddress || msg.sender == address(cdpManager), |
|
| 702 |
"SortedCdps: Caller is neither BO nor CdpM" |
||
| 703 |
); |
||
| 704 |
} |
||
| 705 |
} |
||
| 706 |
| Lines covered: | 33 / 41 (80.5%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./Interfaces/IPriceFeed.sol"; |
||
| 6 |
import "./Interfaces/ICdpManager.sol"; |
||
| 7 |
import "./Interfaces/ISortedCdps.sol"; |
||
| 8 |
import "./Interfaces/ICdpManagerData.sol"; |
||
| 9 |
import "./Dependencies/EbtcBase.sol"; |
||
| 10 | |||
| 11 |
/// @notice Helper to turn a sequence into CDP id array for batch liquidation |
||
| 12 |
contract SyncedLiquidationSequencer is EbtcBase {
|
||
| 13 |
ICdpManager public immutable cdpManager; |
||
| 14 |
ISortedCdps public immutable sortedCdps; |
||
| 15 | |||
| 16 |
constructor( |
||
| 17 |
address _cdpManagerAddress, |
||
| 18 |
address _sortedCdpsAddress, |
||
| 19 |
address _priceFeedAddress, |
||
| 20 |
address _activePoolAddress, |
||
| 21 |
address _collateralAddress |
||
| 22 |
√
|
) EbtcBase(_activePoolAddress, _priceFeedAddress, _collateralAddress) {
|
|
| 23 |
√
|
cdpManager = ICdpManager(_cdpManagerAddress); |
|
| 24 |
√
|
sortedCdps = ISortedCdps(_sortedCdpsAddress); |
|
| 25 |
} |
||
| 26 | |||
| 27 |
/// @dev Get first N batch of liquidatable Cdps at current price |
||
| 28 |
/// @dev Non-view function that updates and returns live price at execution time |
||
| 29 |
function sequenceLiqToBatchLiq(uint256 _n) external returns (bytes32[] memory _array) {
|
||
| 30 |
uint256 _price = priceFeed.fetchPrice(); |
||
| 31 |
return sequenceLiqToBatchLiqWithPrice(_n, _price); |
||
| 32 |
} |
||
| 33 | |||
| 34 |
/// @dev Get first N batch of liquidatable Cdps at specified price |
||
| 35 |
function sequenceLiqToBatchLiqWithPrice( |
||
| 36 |
uint256 _n, |
||
| 37 |
uint256 _price |
||
| 38 |
√
|
) public view returns (bytes32[] memory _array) {
|
|
| 39 |
√
|
(uint256 _TCR, , ) = _getTCRWithSystemDebtAndCollShares(_price); |
|
| 40 |
√
|
bool _recoveryModeAtStart = _TCR < CCR ? true : false; |
|
| 41 |
√
|
return _sequenceLiqToBatchLiq(_n, _recoveryModeAtStart, _price); |
|
| 42 |
} |
||
| 43 | |||
| 44 |
// return CdpId array (in NICR-decreasing order same as SortedCdps) |
||
| 45 |
// including the last N CDPs in sortedCdps for batch liquidation |
||
| 46 |
function _sequenceLiqToBatchLiq( |
||
| 47 |
uint256 _n, |
||
| 48 |
bool _recoveryModeAtStart, |
||
| 49 |
uint256 _price |
||
| 50 |
√
|
) internal view returns (bytes32[] memory _array) {
|
|
| 51 |
√
|
if (_n > 0) {
|
|
| 52 |
√
|
bytes32 _last = sortedCdps.getLast(); |
|
| 53 |
√
|
bytes32 _first = sortedCdps.getFirst(); |
|
| 54 |
√
|
bytes32 _cdpId = _last; |
|
| 55 | |||
| 56 |
√
|
uint256 _TCR = cdpManager.getSyncedTCR(_price); |
|
| 57 | |||
| 58 |
// get count of liquidatable CDPs |
||
| 59 |
√
|
uint256 _cnt; |
|
| 60 |
√
|
for (uint256 i = 0; i < _n && _cdpId != _first; ++i) {
|
|
| 61 |
√
|
uint256 _icr = cdpManager.getSyncedICR(_cdpId, _price); /// @audit This is view ICR and not real ICR |
|
| 62 |
√
|
uint256 _cdpStatus = cdpManager.getCdpStatus(_cdpId); |
|
| 63 |
√
|
bool _liquidatable = _canLiquidateInCurrentMode(_recoveryModeAtStart, _icr, _TCR); |
|
| 64 |
√
|
if (_liquidatable && _cdpStatus == 1) {
|
|
| 65 |
_cnt += 1; |
||
| 66 |
} |
||
| 67 |
√
|
_cdpId = sortedCdps.getPrev(_cdpId); |
|
| 68 |
} |
||
| 69 | |||
| 70 |
// retrieve liquidatable CDPs |
||
| 71 |
√
|
_array = new bytes32[](_cnt); |
|
| 72 |
√
|
_cdpId = _last; |
|
| 73 |
√
|
uint256 _j; |
|
| 74 |
√
|
for (uint256 i = 0; i < _n && _cdpId != _first; ++i) {
|
|
| 75 |
√
|
uint256 _icr = cdpManager.getSyncedICR(_cdpId, _price); |
|
| 76 |
√
|
uint256 _cdpStatus = cdpManager.getCdpStatus(_cdpId); |
|
| 77 |
√
|
bool _liquidatable = _canLiquidateInCurrentMode(_recoveryModeAtStart, _icr, _TCR); |
|
| 78 |
√
|
if (_liquidatable && _cdpStatus == 1) {
|
|
| 79 |
// 1 = ICdpManagerData.Status.active |
||
| 80 |
_array[_cnt - _j - 1] = _cdpId; |
||
| 81 |
_j += 1; |
||
| 82 |
} |
||
| 83 |
√
|
_cdpId = sortedCdps.getPrev(_cdpId); |
|
| 84 |
} |
||
| 85 |
√
|
require(_j == _cnt, "LiquidationLibrary: wrong sequence conversion!"); |
|
| 86 |
} |
||
| 87 |
} |
||
| 88 | |||
| 89 |
function _canLiquidateInCurrentMode( |
||
| 90 |
bool _recovery, |
||
| 91 |
uint256 _icr, |
||
| 92 |
uint256 _TCR |
||
| 93 |
√
|
) internal view returns (bool) {
|
|
| 94 |
√
|
bool _liquidatable = _recovery ? (_icr < MCR || _icr < _TCR) : _icr < MCR; |
|
| 95 | |||
| 96 |
√
|
return _liquidatable; |
|
| 97 |
} |
||
| 98 |
} |
||
| 99 |
| Lines covered: | 6 / 6 (100.0%) |
|---|
| 1 |
// SPDX-License-Identifier: UNLICENSED |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
import {WETH9} from "./WETH9.sol";
|
||
| 5 |
import {BorrowerOperations} from "../BorrowerOperations.sol";
|
||
| 6 |
import {PriceFeedTestnet} from "./testnet/PriceFeedTestnet.sol";
|
||
| 7 |
import {SortedCdps} from "../SortedCdps.sol";
|
||
| 8 |
import {CdpManager} from "../CdpManager.sol";
|
||
| 9 |
import {LiquidationLibrary} from "../LiquidationLibrary.sol";
|
||
| 10 |
import {LiquidationSequencer} from "../LiquidationSequencer.sol";
|
||
| 11 |
import {SyncedLiquidationSequencer} from "../SyncedLiquidationSequencer.sol";
|
||
| 12 |
import {ActivePool} from "../ActivePool.sol";
|
||
| 13 |
import {HintHelpers} from "../HintHelpers.sol";
|
||
| 14 |
import {FeeRecipient} from "../FeeRecipient.sol";
|
||
| 15 |
import {EBTCToken} from "../EBTCToken.sol";
|
||
| 16 |
import {CollSurplusPool} from "../CollSurplusPool.sol";
|
||
| 17 |
import {FunctionCaller} from "./FunctionCaller.sol";
|
||
| 18 |
import {CollateralTokenTester} from "./CollateralTokenTester.sol";
|
||
| 19 |
import {Governor} from "../Governor.sol";
|
||
| 20 |
import {EBTCDeployer} from "../EBTCDeployer.sol";
|
||
| 21 |
import {Actor} from "./invariants/Actor.sol";
|
||
| 22 |
import {CRLens} from "../CRLens.sol";
|
||
| 23 |
import {Simulator} from "./invariants/Simulator.sol";
|
||
| 24 | |||
| 25 |
abstract contract BaseStorageVariables {
|
||
| 26 |
PriceFeedTestnet internal priceFeedMock; |
||
| 27 |
SortedCdps internal sortedCdps; |
||
| 28 |
CdpManager internal cdpManager; |
||
| 29 |
WETH9 internal weth; |
||
| 30 |
ActivePool internal activePool; |
||
| 31 |
CollSurplusPool internal collSurplusPool; |
||
| 32 |
FunctionCaller internal functionCaller; |
||
| 33 |
BorrowerOperations internal borrowerOperations; |
||
| 34 |
HintHelpers internal hintHelpers; |
||
| 35 |
EBTCToken internal eBTCToken; |
||
| 36 |
CollateralTokenTester internal collateral; |
||
| 37 |
Governor internal authority; |
||
| 38 |
LiquidationLibrary internal liqudationLibrary; |
||
| 39 |
LiquidationSequencer internal liquidationSequencer; |
||
| 40 |
SyncedLiquidationSequencer internal syncedLiquidationSequencer; |
||
| 41 |
EBTCDeployer internal ebtcDeployer; |
||
| 42 |
address internal defaultGovernance; |
||
| 43 | |||
| 44 |
// LQTY Stuff |
||
| 45 |
FeeRecipient internal feeRecipient; |
||
| 46 | |||
| 47 |
mapping(address => Actor) internal actors; |
||
| 48 |
Actor internal actor; |
||
| 49 | |||
| 50 |
CRLens internal crLens; |
||
| 51 |
Simulator internal simulator; |
||
| 52 | |||
| 53 |
√
|
uint internal constant NUMBER_OF_ACTORS = 3; |
|
| 54 |
√
|
uint internal constant INITIAL_ETH_BALANCE = 1e24; |
|
| 55 |
√
|
⟳
|
uint internal constant INITIAL_COLL_BALANCE = 1e21; |
| 56 | |||
| 57 |
√
|
uint internal constant diff_tolerance = 0.000000000002e18; //compared to 1e18 |
|
| 58 |
√
|
uint internal constant MAX_PRICE_CHANGE_PERCENT = 1.05e18; //compared to 1e18 |
|
| 59 |
√
|
uint internal constant MAX_REBASE_PERCENT = 1.1e18; //compared to 1e18 |
|
| 60 |
uint internal constant MAX_FLASHLOAN_ACTIONS = 4; |
||
| 61 |
} |
||
| 62 |
| Lines covered: | 58 / 115 (50.4%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity 0.8.17; |
||
| 3 | |||
| 4 |
import "../Dependencies/ICollateralToken.sol"; |
||
| 5 |
import "../Dependencies/ICollateralTokenOracle.sol"; |
||
| 6 |
import "../Dependencies/Ownable.sol"; |
||
| 7 | |||
| 8 |
interface IEbtcInternalPool {
|
||
| 9 |
function increaseSystemCollShares(uint256 _value) external; |
||
| 10 |
} |
||
| 11 | |||
| 12 |
// based on WETH9 contract |
||
| 13 |
contract CollateralTokenTester is ICollateralToken, ICollateralTokenOracle, Ownable {
|
||
| 14 |
√
|
string public override name = "Collateral Token Tester in eBTC"; |
|
| 15 |
√
|
string public override symbol = "CollTester"; |
|
| 16 |
√
|
uint8 public override decimals = 18; |
|
| 17 | |||
| 18 |
event TransferShares(address indexed from, address indexed to, uint256 sharesValue); |
||
| 19 |
event Deposit(address indexed dst, uint256 wad, uint256 _share); |
||
| 20 |
event Withdrawal(address indexed src, uint256 wad, uint256 _share); |
||
| 21 |
event UncappedMinterAdded(address indexed account); |
||
| 22 |
event UncappedMinterRemoved(address indexed account); |
||
| 23 |
event MintCapSet(uint256 indexed newCap); |
||
| 24 |
event MintCooldownSet(uint256 indexed newCooldown); |
||
| 25 | |||
| 26 |
mapping(address => uint256) private balances; |
||
| 27 |
mapping(address => mapping(address => uint256)) public override allowance; |
||
| 28 |
mapping(address => bool) public isUncappedMinter; |
||
| 29 |
mapping(address => uint256) public lastMintTime; |
||
| 30 | |||
| 31 |
// Faucet capped at 10 Collateral tokens per day |
||
| 32 |
√
|
uint256 public mintCap = 10e18; |
|
| 33 |
√
|
uint256 public mintCooldown = 60 * 60 * 24; |
|
| 34 | |||
| 35 |
√
|
uint256 private _ethPerShare = 1e18; |
|
| 36 |
uint256 private _totalBalance; |
||
| 37 | |||
| 38 |
√
|
uint256 private epochsPerFrame = 225; |
|
| 39 |
√
|
uint256 private slotsPerEpoch = 32; |
|
| 40 |
√
|
uint256 private secondsPerSlot = 12; |
|
| 41 | |||
| 42 |
receive() external payable {
|
||
| 43 |
deposit(); |
||
| 44 |
} |
||
| 45 | |||
| 46 |
function deposit() public payable {
|
||
| 47 |
√
|
uint256 _share = getSharesByPooledEth(msg.value); |
|
| 48 |
√
|
balances[msg.sender] += _share; |
|
| 49 |
√
|
_totalBalance += _share; |
|
| 50 |
√
|
emit Deposit(msg.sender, msg.value, _share); |
|
| 51 |
} |
||
| 52 | |||
| 53 |
/// @dev Deposit collateral without ether for testing purposes |
||
| 54 |
function forceDeposit(uint256 ethToDeposit) external {
|
||
| 55 |
if (!isUncappedMinter[msg.sender]) {
|
||
| 56 |
require(ethToDeposit <= mintCap, "CollTester: Above mint cap"); |
||
| 57 |
require( |
||
| 58 |
lastMintTime[msg.sender] == 0 || |
||
| 59 |
lastMintTime[msg.sender] + mintCooldown < block.timestamp, |
||
| 60 |
"CollTester: Cooldown period not completed" |
||
| 61 |
); |
||
| 62 |
lastMintTime[msg.sender] = block.timestamp; |
||
| 63 |
} |
||
| 64 |
uint256 _share = getSharesByPooledEth(ethToDeposit); |
||
| 65 |
balances[msg.sender] += _share; |
||
| 66 |
_totalBalance += _share; |
||
| 67 |
emit Deposit(msg.sender, ethToDeposit, _share); |
||
| 68 |
} |
||
| 69 | |||
| 70 |
function withdraw(uint256 wad) public {
|
||
| 71 |
uint256 _share = getSharesByPooledEth(wad); |
||
| 72 |
require(balances[msg.sender] >= _share); |
||
| 73 |
balances[msg.sender] -= _share; |
||
| 74 |
_totalBalance -= _share; |
||
| 75 |
payable(msg.sender).transfer(wad); |
||
| 76 |
emit Withdrawal(msg.sender, wad, _share); |
||
| 77 |
} |
||
| 78 | |||
| 79 |
function totalSupply() public view override returns (uint) {
|
||
| 80 |
uint _tmp = _mul(_ethPerShare, _totalBalance); |
||
| 81 |
return _div(_tmp, 1e18); |
||
| 82 |
} |
||
| 83 | |||
| 84 |
// Permissioned functions |
||
| 85 |
function addUncappedMinter(address account) external onlyOwner {
|
||
| 86 |
isUncappedMinter[account] = true; |
||
| 87 |
emit UncappedMinterAdded(account); |
||
| 88 |
} |
||
| 89 | |||
| 90 |
function removeUncappedMinter(address account) external onlyOwner {
|
||
| 91 |
isUncappedMinter[account] = false; |
||
| 92 |
emit UncappedMinterRemoved(account); |
||
| 93 |
} |
||
| 94 | |||
| 95 |
function setMintCap(uint256 newCap) external onlyOwner {
|
||
| 96 |
mintCap = newCap; |
||
| 97 |
emit MintCapSet(newCap); |
||
| 98 |
} |
||
| 99 | |||
| 100 |
function setMintCooldown(uint256 newCooldown) external onlyOwner {
|
||
| 101 |
mintCooldown = newCooldown; |
||
| 102 |
emit MintCooldownSet(newCooldown); |
||
| 103 |
} |
||
| 104 | |||
| 105 |
// helper to set allowance in test |
||
| 106 |
function nonStandardSetApproval( |
||
| 107 |
address owner, |
||
| 108 |
address guy, |
||
| 109 |
uint256 wad |
||
| 110 |
) external returns (bool) {
|
||
| 111 |
allowance[owner][guy] = wad; |
||
| 112 |
emit Approval(owner, guy, wad); |
||
| 113 |
return true; |
||
| 114 |
} |
||
| 115 | |||
| 116 |
√
|
⟳
|
function approve(address guy, uint256 wad) public override returns (bool) {
|
| 117 |
√
|
⟳
|
allowance[msg.sender][guy] = wad; |
| 118 |
√
|
⟳
|
emit Approval(msg.sender, guy, wad); |
| 119 |
√
|
⟳
|
return true; |
| 120 |
} |
||
| 121 | |||
| 122 |
function transfer(address dst, uint256 wad) public override returns (bool) {
|
||
| 123 |
return transferFrom(msg.sender, dst, wad); |
||
| 124 |
} |
||
| 125 | |||
| 126 |
√
|
function transferFrom(address src, address dst, uint256 wad) public override returns (bool) {
|
|
| 127 |
√
|
uint256 _share = getSharesByPooledEth(wad); |
|
| 128 |
√
|
require(balances[src] >= _share, "ERC20: transfer amount exceeds balance"); |
|
| 129 | |||
| 130 |
√
|
if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) {
|
|
| 131 |
√
|
require(allowance[src][msg.sender] >= wad); |
|
| 132 |
√
|
allowance[src][msg.sender] -= wad; |
|
| 133 |
} |
||
| 134 | |||
| 135 |
√
|
balances[src] -= _share; |
|
| 136 |
√
|
balances[dst] += _share; |
|
| 137 | |||
| 138 |
√
|
_emitTransferEvents(src, dst, wad, _share); |
|
| 139 | |||
| 140 |
√
|
return true; |
|
| 141 |
} |
||
| 142 | |||
| 143 |
// tests should adjust the ratio by this function |
||
| 144 |
function setEthPerShare(uint256 _ePerS) external {
|
||
| 145 |
√
|
_ethPerShare = _ePerS; |
|
| 146 |
} |
||
| 147 | |||
| 148 |
√
|
⟳
|
function getEthPerShare() external view returns (uint256) {
|
| 149 |
√
|
⟳
|
return _ethPerShare; |
| 150 |
} |
||
| 151 | |||
| 152 |
√
|
⟳
|
function getSharesByPooledEth(uint256 _ethAmount) public view override returns (uint256) {
|
| 153 |
√
|
⟳
|
uint256 _tmp = _mul(1e18, _ethAmount); |
| 154 |
√
|
⟳
|
return _div(_tmp, _ethPerShare); |
| 155 |
} |
||
| 156 | |||
| 157 |
√
|
⟳
|
function getPooledEthByShares(uint256 _sharesAmount) public view override returns (uint256) {
|
| 158 |
√
|
⟳
|
uint256 _tmp = _mul(_ethPerShare, _sharesAmount); |
| 159 |
√
|
⟳
|
return _div(_tmp, 1e18); |
| 160 |
} |
||
| 161 | |||
| 162 |
function transferShares( |
||
| 163 |
address _recipient, |
||
| 164 |
uint256 _sharesAmount |
||
| 165 |
√
|
⟳
|
) public override returns (uint256) {
|
| 166 |
√
|
⟳
|
uint256 _tknAmt = getPooledEthByShares(_sharesAmount); |
| 167 | |||
| 168 |
// NOTE: Changed here to transfer underlying shares without rounding |
||
| 169 |
√
|
⟳
|
balances[msg.sender] -= _sharesAmount; |
| 170 |
√
|
⟳
|
balances[_recipient] += _sharesAmount; |
| 171 | |||
| 172 |
√
|
⟳
|
_emitTransferEvents(msg.sender, _recipient, _tknAmt, _sharesAmount); |
| 173 | |||
| 174 |
√
|
⟳
|
return _tknAmt; |
| 175 |
} |
||
| 176 | |||
| 177 |
√
|
⟳
|
function sharesOf(address _account) public view override returns (uint256) {
|
| 178 |
√
|
⟳
|
return balances[_account]; |
| 179 |
} |
||
| 180 | |||
| 181 |
function getOracle() external view override returns (address) {
|
||
| 182 |
return address(this); |
||
| 183 |
} |
||
| 184 | |||
| 185 |
function getBeaconSpec() public view override returns (uint64, uint64, uint64, uint64) {
|
||
| 186 |
return ( |
||
| 187 |
uint64(epochsPerFrame), |
||
| 188 |
uint64(slotsPerEpoch), |
||
| 189 |
uint64(secondsPerSlot), |
||
| 190 |
uint64(block.timestamp) |
||
| 191 |
); |
||
| 192 |
} |
||
| 193 | |||
| 194 |
function setBeaconSpec( |
||
| 195 |
uint64 _epochsPerFrame, |
||
| 196 |
uint64 _slotsPerEpoch, |
||
| 197 |
uint64 _secondsPerSlot |
||
| 198 |
) external {
|
||
| 199 |
epochsPerFrame = _epochsPerFrame; |
||
| 200 |
slotsPerEpoch = _slotsPerEpoch; |
||
| 201 |
secondsPerSlot = _secondsPerSlot; |
||
| 202 |
} |
||
| 203 | |||
| 204 |
function decreaseAllowance( |
||
| 205 |
address spender, |
||
| 206 |
uint256 subtractedValue |
||
| 207 |
) external override returns (bool) {
|
||
| 208 |
approve(spender, allowance[msg.sender][spender] - subtractedValue); |
||
| 209 |
return true; |
||
| 210 |
} |
||
| 211 | |||
| 212 |
√
|
⟳
|
function balanceOf(address _usr) external view override returns (uint256) {
|
| 213 |
√
|
⟳
|
uint256 _tmp = _mul(_ethPerShare, balances[_usr]); |
| 214 |
√
|
⟳
|
return _div(_tmp, 1e18); |
| 215 |
} |
||
| 216 | |||
| 217 |
function increaseAllowance( |
||
| 218 |
address spender, |
||
| 219 |
uint256 addedValue |
||
| 220 |
) external override returns (bool) {
|
||
| 221 |
approve(spender, allowance[msg.sender][spender] + addedValue); |
||
| 222 |
return true; |
||
| 223 |
} |
||
| 224 | |||
| 225 |
// internal helper functions |
||
| 226 |
√
|
⟳
|
function _mul(uint256 a, uint256 b) internal pure returns (uint256) {
|
| 227 |
√
|
⟳
|
if (a == 0) {
|
| 228 |
return 0; |
||
| 229 |
} |
||
| 230 |
√
|
⟳
|
uint256 c = a * b; |
| 231 |
√
|
⟳
|
require(c / a == b, "SafeMath: multiplication overflow"); |
| 232 |
√
|
⟳
|
return c; |
| 233 |
} |
||
| 234 | |||
| 235 |
√
|
⟳
|
function _div(uint256 a, uint256 b) internal pure returns (uint256) {
|
| 236 |
√
|
⟳
|
require(b > 0, "SafeMath: zero denominator"); |
| 237 |
√
|
⟳
|
uint256 c = a / b; |
| 238 |
√
|
⟳
|
return c; |
| 239 |
} |
||
| 240 | |||
| 241 |
// dummy test purpose |
||
| 242 |
function feeRecipientAddress() external view returns (address) {
|
||
| 243 |
return address(this); |
||
| 244 |
} |
||
| 245 | |||
| 246 |
function authority() external view returns (address) {
|
||
| 247 |
return address(this); |
||
| 248 |
} |
||
| 249 | |||
| 250 |
/** |
||
| 251 |
* @dev Emits {Transfer} and {TransferShares} events
|
||
| 252 |
*/ |
||
| 253 |
function _emitTransferEvents( |
||
| 254 |
address _from, |
||
| 255 |
address _to, |
||
| 256 |
uint _tokenAmount, |
||
| 257 |
uint256 _sharesAmount |
||
| 258 |
) internal {
|
||
| 259 |
√
|
⟳
|
emit Transfer(_from, _to, _tokenAmount); |
| 260 |
√
|
⟳
|
emit TransferShares(_from, _to, _sharesAmount); |
| 261 |
} |
||
| 262 |
} |
||
| 263 |
| Lines covered: | 2 / 15 (13.3%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "../EBTCToken.sol"; |
||
| 6 | |||
| 7 |
contract EBTCTokenTester is EBTCToken {
|
||
| 8 |
bytes32 private immutable _PERMIT_TYPEHASH = |
||
| 9 |
√
|
0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; |
|
| 10 | |||
| 11 |
constructor( |
||
| 12 |
address _cdpManagerAddress, |
||
| 13 |
address _borrowerOperationsAddress, |
||
| 14 |
address _authorityAddress |
||
| 15 |
√
|
) EBTCToken(_cdpManagerAddress, _borrowerOperationsAddress, _authorityAddress) {}
|
|
| 16 | |||
| 17 |
function unprotectedMint(address _account, uint256 _amount) external {
|
||
| 18 |
// No check on caller here |
||
| 19 | |||
| 20 |
_mint(_account, _amount); |
||
| 21 |
} |
||
| 22 | |||
| 23 |
function unprotectedBurn(address _account, uint256 _amount) external {
|
||
| 24 |
// No check on caller here |
||
| 25 | |||
| 26 |
_burn(_account, _amount); |
||
| 27 |
} |
||
| 28 | |||
| 29 |
function unprotectedSendToPool(address _sender, address _poolAddress, uint256 _amount) external {
|
||
| 30 |
// No check on caller here |
||
| 31 | |||
| 32 |
_transfer(_sender, _poolAddress, _amount); |
||
| 33 |
} |
||
| 34 | |||
| 35 |
function unprotectedReturnFromPool( |
||
| 36 |
address _poolAddress, |
||
| 37 |
address _receiver, |
||
| 38 |
uint256 _amount |
||
| 39 |
) external {
|
||
| 40 |
// No check on caller here |
||
| 41 | |||
| 42 |
_transfer(_poolAddress, _receiver, _amount); |
||
| 43 |
} |
||
| 44 | |||
| 45 |
function callInternalApprove(address owner, address spender, uint256 amount) external {
|
||
| 46 |
_approve(owner, spender, amount); |
||
| 47 |
} |
||
| 48 | |||
| 49 |
function getChainId() external view returns (uint256 chainID) {
|
||
| 50 |
//return _chainID(); // it’s private |
||
| 51 |
assembly {
|
||
| 52 |
chainID := chainid() |
||
| 53 |
} |
||
| 54 |
} |
||
| 55 | |||
| 56 |
function getDigest( |
||
| 57 |
address owner, |
||
| 58 |
address spender, |
||
| 59 |
uint256 amount, |
||
| 60 |
uint256 nonce, |
||
| 61 |
uint256 deadline |
||
| 62 |
) external view returns (bytes32) {
|
||
| 63 |
return |
||
| 64 |
keccak256( |
||
| 65 |
abi.encodePacked( |
||
| 66 |
uint16(0x1901), |
||
| 67 |
domainSeparator(), |
||
| 68 |
keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, amount, nonce, deadline)) |
||
| 69 |
) |
||
| 70 |
); |
||
| 71 |
} |
||
| 72 | |||
| 73 |
function recoverAddress( |
||
| 74 |
bytes32 digest, |
||
| 75 |
uint8 v, |
||
| 76 |
bytes32 r, |
||
| 77 |
bytes32 s |
||
| 78 |
) external pure returns (address) {
|
||
| 79 |
return ecrecover(digest, v, r, s); |
||
| 80 |
} |
||
| 81 |
} |
||
| 82 |
| Lines covered: | 0 / 14 (0.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "../Interfaces/ICdpManager.sol"; |
||
| 6 |
import "../Interfaces/ISortedCdps.sol"; |
||
| 7 |
import "../Interfaces/IPriceFeed.sol"; |
||
| 8 |
import "../Dependencies/EbtcMath.sol"; |
||
| 9 | |||
| 10 |
/* Wrapper contract - used for calculating gas of read-only and internal functions. |
||
| 11 |
Not part of the Liquity application. */ |
||
| 12 |
contract FunctionCaller {
|
||
| 13 |
ICdpManager cdpManager; |
||
| 14 |
address public cdpManagerAddress; |
||
| 15 | |||
| 16 |
ISortedCdps sortedCdps; |
||
| 17 |
address public sortedCdpsAddress; |
||
| 18 | |||
| 19 |
IPriceFeed priceFeed; |
||
| 20 |
address public priceFeedAddress; |
||
| 21 | |||
| 22 |
// --- Dependency setters --- |
||
| 23 | |||
| 24 |
function setCdpManagerAddress(address _cdpManagerAddress) external {
|
||
| 25 |
cdpManagerAddress = _cdpManagerAddress; |
||
| 26 |
cdpManager = ICdpManager(_cdpManagerAddress); |
||
| 27 |
} |
||
| 28 | |||
| 29 |
function setSortedCdpsAddress(address _sortedCdpsAddress) external {
|
||
| 30 |
cdpManagerAddress = _sortedCdpsAddress; |
||
| 31 |
sortedCdps = ISortedCdps(_sortedCdpsAddress); |
||
| 32 |
} |
||
| 33 | |||
| 34 |
function setPriceFeedAddress(address _priceFeedAddress) external {
|
||
| 35 |
priceFeedAddress = _priceFeedAddress; |
||
| 36 |
priceFeed = IPriceFeed(_priceFeedAddress); |
||
| 37 |
} |
||
| 38 | |||
| 39 |
// --- Non-view wrapper functions used for calculating gas --- |
||
| 40 | |||
| 41 |
function cdpManager_getICR(bytes32 _cdpId, uint256 _price) external view returns (uint256) {
|
||
| 42 |
return cdpManager.getICR(_cdpId, _price); |
||
| 43 |
} |
||
| 44 | |||
| 45 |
function sortedCdps_findInsertPosition( |
||
| 46 |
uint256 _NICR, |
||
| 47 |
bytes32 _prevId, |
||
| 48 |
bytes32 _nextId |
||
| 49 |
) external view returns (bytes32, bytes32) {
|
||
| 50 |
return sortedCdps.findInsertPosition(_NICR, _prevId, _nextId); |
||
| 51 |
} |
||
| 52 |
} |
||
| 53 |
| Lines covered: | 0 / 91 (0.0%) |
|---|
| 1 |
// https://github.com/one-hundred-proof/kyberswap-exploit/blob/main/lib/helpers/Pretty.sol |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
library Strings {
|
||
| 6 |
function concat( |
||
| 7 |
string memory _base, |
||
| 8 |
string memory _value |
||
| 9 |
) internal pure returns (string memory) {
|
||
| 10 |
bytes memory _baseBytes = bytes(_base); |
||
| 11 |
bytes memory _valueBytes = bytes(_value); |
||
| 12 | |||
| 13 |
string memory _tmpValue = new string(_baseBytes.length + _valueBytes.length); |
||
| 14 |
bytes memory _newValue = bytes(_tmpValue); |
||
| 15 | |||
| 16 |
uint i; |
||
| 17 |
uint j; |
||
| 18 | |||
| 19 |
for (i = 0; i < _baseBytes.length; i++) {
|
||
| 20 |
_newValue[j++] = _baseBytes[i]; |
||
| 21 |
} |
||
| 22 | |||
| 23 |
for (i = 0; i < _valueBytes.length; i++) {
|
||
| 24 |
_newValue[j++] = _valueBytes[i]; |
||
| 25 |
} |
||
| 26 | |||
| 27 |
return string(_newValue); |
||
| 28 |
} |
||
| 29 |
} |
||
| 30 | |||
| 31 |
library Pretty {
|
||
| 32 |
uint8 constant DEFAULT_DECIMALS = 18; |
||
| 33 | |||
| 34 |
function toBitString(uint256 n) external pure returns (string memory) {
|
||
| 35 |
return uintToBitString(n, 256); |
||
| 36 |
} |
||
| 37 | |||
| 38 |
function toBitString(uint256 n, uint8 decimals) external pure returns (string memory) {
|
||
| 39 |
return uintToBitString(n, decimals); |
||
| 40 |
} |
||
| 41 | |||
| 42 |
function pretty(uint256 n) external pure returns (string memory) {
|
||
| 43 |
return |
||
| 44 |
n == type(uint256).max ? "type(uint256).max" : n == type(uint128).max |
||
| 45 |
? "type(uint128).max" |
||
| 46 |
: _pretty(n, DEFAULT_DECIMALS); |
||
| 47 |
} |
||
| 48 | |||
| 49 |
function pretty(bool value) external pure returns (string memory) {
|
||
| 50 |
return value ? "true" : "false"; |
||
| 51 |
} |
||
| 52 | |||
| 53 |
function pretty(uint256 n, uint8 decimals) external pure returns (string memory) {
|
||
| 54 |
return _pretty(n, decimals); |
||
| 55 |
} |
||
| 56 | |||
| 57 |
function pretty(int256 n) external pure returns (string memory) {
|
||
| 58 |
return _prettyInt(n, DEFAULT_DECIMALS); |
||
| 59 |
} |
||
| 60 | |||
| 61 |
function pretty(int256 n, uint8 decimals) external pure returns (string memory) {
|
||
| 62 |
return _prettyInt(n, decimals); |
||
| 63 |
} |
||
| 64 | |||
| 65 |
function _pretty(uint256 n, uint8 decimals) internal pure returns (string memory) {
|
||
| 66 |
bool pastDecimals = decimals == 0; |
||
| 67 |
uint256 place = 0; |
||
| 68 |
uint256 r; // remainder |
||
| 69 |
string memory s = ""; |
||
| 70 | |||
| 71 |
while (n != 0) {
|
||
| 72 |
r = n % 10; |
||
| 73 |
n /= 10; |
||
| 74 |
place++; |
||
| 75 |
s = Strings.concat(toDigit(r), s); |
||
| 76 |
if (pastDecimals && place % 3 == 0 && n != 0) {
|
||
| 77 |
s = Strings.concat("_", s);
|
||
| 78 |
} |
||
| 79 |
if (!pastDecimals && place == decimals) {
|
||
| 80 |
pastDecimals = true; |
||
| 81 |
place = 0; |
||
| 82 |
s = Strings.concat("_", s);
|
||
| 83 |
} |
||
| 84 |
} |
||
| 85 |
if (pastDecimals && place == 0) {
|
||
| 86 |
s = Strings.concat("0", s);
|
||
| 87 |
} |
||
| 88 |
if (!pastDecimals) {
|
||
| 89 |
uint256 i; |
||
| 90 |
uint256 upper = (decimals >= place ? decimals - place : 0); |
||
| 91 |
for (i = 0; i < upper; ++i) {
|
||
| 92 |
s = Strings.concat("0", s);
|
||
| 93 |
} |
||
| 94 |
s = Strings.concat("0_", s);
|
||
| 95 |
} |
||
| 96 |
return s; |
||
| 97 |
} |
||
| 98 | |||
| 99 |
function _prettyInt(int256 n, uint8 decimals) internal pure returns (string memory) {
|
||
| 100 |
bool isNegative = n < 0; |
||
| 101 |
string memory s = ""; |
||
| 102 |
if (isNegative) {
|
||
| 103 |
s = "-"; |
||
| 104 |
} |
||
| 105 |
return Strings.concat(s, _pretty(uint256(isNegative ? -n : n), decimals)); |
||
| 106 |
} |
||
| 107 | |||
| 108 |
function toDigit(uint256 n) internal pure returns (string memory) {
|
||
| 109 |
if (n == 0) {
|
||
| 110 |
return "0"; |
||
| 111 |
} else if (n == 1) {
|
||
| 112 |
return "1"; |
||
| 113 |
} else if (n == 2) {
|
||
| 114 |
return "2"; |
||
| 115 |
} else if (n == 3) {
|
||
| 116 |
return "3"; |
||
| 117 |
} else if (n == 4) {
|
||
| 118 |
return "4"; |
||
| 119 |
} else if (n == 5) {
|
||
| 120 |
return "5"; |
||
| 121 |
} else if (n == 6) {
|
||
| 122 |
return "6"; |
||
| 123 |
} else if (n == 7) {
|
||
| 124 |
return "7"; |
||
| 125 |
} else if (n == 8) {
|
||
| 126 |
return "8"; |
||
| 127 |
} else if (n == 9) {
|
||
| 128 |
return "9"; |
||
| 129 |
} else {
|
||
| 130 |
revert("Not in range 0 to 10");
|
||
| 131 |
} |
||
| 132 |
} |
||
| 133 | |||
| 134 |
function uintToBitString(uint256 n, uint16 bits) internal pure returns (string memory) {
|
||
| 135 |
string memory s = ""; |
||
| 136 |
for (uint256 i; i < bits; i++) {
|
||
| 137 |
if (n % 2 == 0) {
|
||
| 138 |
s = Strings.concat("0", s);
|
||
| 139 |
} else {
|
||
| 140 |
s = Strings.concat("1", s);
|
||
| 141 |
} |
||
| 142 |
n = n / 2; |
||
| 143 |
} |
||
| 144 |
return s; |
||
| 145 |
} |
||
| 146 |
} |
||
| 147 |
| Lines covered: | 0 / 29 (0.0%) |
|---|
| 1 |
/** |
||
| 2 |
*Submitted for verification at Etherscan.io on 2017-12-12 |
||
| 3 |
*/ |
||
| 4 | |||
| 5 |
// Copyright (C) 2015, 2016, 2017 Dapphub |
||
| 6 | |||
| 7 |
// This program is free software: you can redistribute it and/or modify |
||
| 8 |
// it under the terms of the GNU General Public License as published by |
||
| 9 |
// the Free Software Foundation, either version 3 of the License, or |
||
| 10 |
// (at your option) any later version. |
||
| 11 | |||
| 12 |
// This program is distributed in the hope that it will be useful, |
||
| 13 |
// but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 14 |
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 15 |
// GNU General Public License for more details. |
||
| 16 | |||
| 17 |
// You should have received a copy of the GNU General Public License |
||
| 18 |
// along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
| 19 | |||
| 20 |
pragma solidity 0.8.17; |
||
| 21 | |||
| 22 |
contract WETH9 {
|
||
| 23 |
string public name = "Wrapped Ether"; |
||
| 24 |
string public symbol = "WETH"; |
||
| 25 |
uint8 public decimals = 18; |
||
| 26 | |||
| 27 |
event Approval(address indexed src, address indexed guy, uint256 wad); |
||
| 28 |
event Transfer(address indexed src, address indexed dst, uint256 wad); |
||
| 29 |
event Deposit(address indexed dst, uint256 wad); |
||
| 30 |
event Withdrawal(address indexed src, uint256 wad); |
||
| 31 | |||
| 32 |
mapping(address => uint256) public balanceOf; |
||
| 33 |
mapping(address => mapping(address => uint256)) public allowance; |
||
| 34 | |||
| 35 |
function receive() public payable {
|
||
| 36 |
deposit(); |
||
| 37 |
} |
||
| 38 | |||
| 39 |
function deposit() public payable {
|
||
| 40 |
balanceOf[msg.sender] += msg.value; |
||
| 41 |
emit Deposit(msg.sender, msg.value); |
||
| 42 |
} |
||
| 43 | |||
| 44 |
function withdraw(uint256 wad) public {
|
||
| 45 |
require(balanceOf[msg.sender] >= wad); |
||
| 46 |
balanceOf[msg.sender] -= wad; |
||
| 47 |
payable(msg.sender).transfer(wad); |
||
| 48 |
emit Withdrawal(msg.sender, wad); |
||
| 49 |
} |
||
| 50 | |||
| 51 |
function totalSupply() public view returns (uint256) {
|
||
| 52 |
return address(this).balance; |
||
| 53 |
} |
||
| 54 | |||
| 55 |
function approve(address guy, uint256 wad) public returns (bool) {
|
||
| 56 |
allowance[msg.sender][guy] = wad; |
||
| 57 |
emit Approval(msg.sender, guy, wad); |
||
| 58 |
return true; |
||
| 59 |
} |
||
| 60 | |||
| 61 |
function transfer(address dst, uint256 wad) public virtual returns (bool) {
|
||
| 62 |
return transferFrom(msg.sender, dst, wad); |
||
| 63 |
} |
||
| 64 | |||
| 65 |
function transferFrom(address src, address dst, uint256 wad) public returns (bool) {
|
||
| 66 |
require(balanceOf[src] >= wad); |
||
| 67 | |||
| 68 |
if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) {
|
||
| 69 |
require(allowance[src][msg.sender] >= wad); |
||
| 70 |
allowance[src][msg.sender] -= wad; |
||
| 71 |
} |
||
| 72 | |||
| 73 |
balanceOf[src] -= wad; |
||
| 74 |
balanceOf[dst] += wad; |
||
| 75 | |||
| 76 |
emit Transfer(src, dst, wad); |
||
| 77 | |||
| 78 |
return true; |
||
| 79 |
} |
||
| 80 |
} |
||
| 81 | |||
| 82 |
/* |
||
| 83 |
GNU GENERAL PUBLIC LICENSE |
||
| 84 |
Version 3, 29 June 2007 |
||
| 85 | |||
| 86 |
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> |
||
| 87 |
Everyone is permitted to copy and distribute verbatim copies |
||
| 88 |
of this license document, but changing it is not allowed. |
||
| 89 | |||
| 90 |
Preamble |
||
| 91 | |||
| 92 |
The GNU General Public License is a free, copyleft license for |
||
| 93 |
software and other kinds of works. |
||
| 94 | |||
| 95 |
The licenses for most software and other practical works are designed |
||
| 96 |
to take away your freedom to share and change the works. By contrast, |
||
| 97 |
the GNU General Public License is intended to guarantee your freedom to |
||
| 98 |
share and change all versions of a program--to make sure it remains free |
||
| 99 |
software for all its users. We, the Free Software Foundation, use the |
||
| 100 |
GNU General Public License for most of our software; it applies also to |
||
| 101 |
any other work released this way by its authors. You can apply it to |
||
| 102 |
your programs, too. |
||
| 103 | |||
| 104 |
When we speak of free software, we are referring to freedom, not |
||
| 105 |
price. Our General Public Licenses are designed to make sure that you |
||
| 106 |
have the freedom to distribute copies of free software (and charge for |
||
| 107 |
them if you wish), that you receive source code or can get it if you |
||
| 108 |
want it, that you can change the software or use pieces of it in new |
||
| 109 |
free programs, and that you know you can do these things. |
||
| 110 | |||
| 111 |
To protect your rights, we need to prevent others from denying you |
||
| 112 |
these rights or asking you to surrender the rights. Therefore, you have |
||
| 113 |
certain responsibilities if you distribute copies of the software, or if |
||
| 114 |
you modify it: responsibilities to respect the freedom of others. |
||
| 115 | |||
| 116 |
For example, if you distribute copies of such a program, whether |
||
| 117 |
gratis or for a fee, you must pass on to the recipients the same |
||
| 118 |
freedoms that you received. You must make sure that they, too, receive |
||
| 119 |
or can get the source code. And you must show them these terms so they |
||
| 120 |
know their rights. |
||
| 121 | |||
| 122 |
Developers that use the GNU GPL protect your rights with two steps: |
||
| 123 |
(1) assert copyright on the software, and (2) offer you this License |
||
| 124 |
giving you legal permission to copy, distribute and/or modify it. |
||
| 125 | |||
| 126 |
For the developers' and authors' protection, the GPL clearly explains |
||
| 127 |
that there is no warranty for this free software. For both users' and |
||
| 128 |
authors' sake, the GPL requires that modified versions be marked as |
||
| 129 |
changed, so that their problems will not be attributed erroneously to |
||
| 130 |
authors of previous versions. |
||
| 131 | |||
| 132 |
Some devices are designed to deny users access to install or run |
||
| 133 |
modified versions of the software inside them, although the manufacturer |
||
| 134 |
can do so. This is fundamentally incompatible with the aim of |
||
| 135 |
protecting users' freedom to change the software. The systematic |
||
| 136 |
pattern of such abuse occurs in the area of products for individuals to |
||
| 137 |
use, which is precisely where it is most unacceptable. Therefore, we |
||
| 138 |
have designed this version of the GPL to prohibit the practice for those |
||
| 139 |
products. If such problems arise substantially in other domains, we |
||
| 140 |
stand ready to extend this provision to those domains in future versions |
||
| 141 |
of the GPL, as needed to protect the freedom of users. |
||
| 142 | |||
| 143 |
Finally, every program is threatened constantly by software patents. |
||
| 144 |
States should not allow patents to restrict development and use of |
||
| 145 |
software on general-purpose computers, but in those that do, we wish to |
||
| 146 |
avoid the special danger that patents applied to a free program could |
||
| 147 |
make it effectively proprietary. To prevent this, the GPL assures that |
||
| 148 |
patents cannot be used to render the program non-free. |
||
| 149 | |||
| 150 |
The precise terms and conditions for copying, distribution and |
||
| 151 |
modification follow. |
||
| 152 | |||
| 153 |
TERMS AND CONDITIONS |
||
| 154 | |||
| 155 |
0. Definitions. |
||
| 156 | |||
| 157 |
"This License" refers to version 3 of the GNU General Public License. |
||
| 158 | |||
| 159 |
"Copyright" also means copyright-like laws that apply to other kinds of |
||
| 160 |
works, such as semiconductor masks. |
||
| 161 | |||
| 162 |
"The Program" refers to any copyrightable work licensed under this |
||
| 163 |
License. Each licensee is addressed as "you". "Licensees" and |
||
| 164 |
"recipients" may be individuals or organizations. |
||
| 165 | |||
| 166 |
To "modify" a work means to copy from or adapt all or part of the work |
||
| 167 |
in a fashion requiring copyright permission, other than the making of an |
||
| 168 |
exact copy. The resulting work is called a "modified version" of the |
||
| 169 |
earlier work or a work "based on" the earlier work. |
||
| 170 | |||
| 171 |
A "covered work" means either the unmodified Program or a work based |
||
| 172 |
on the Program. |
||
| 173 | |||
| 174 |
To "propagate" a work means to do anything with it that, without |
||
| 175 |
permission, would make you directly or secondarily liable for |
||
| 176 |
infringement under applicable copyright law, except executing it on a |
||
| 177 |
computer or modifying a private copy. Propagation includes copying, |
||
| 178 |
distribution (with or without modification), making available to the |
||
| 179 |
public, and in some countries other activities as well. |
||
| 180 | |||
| 181 |
To "convey" a work means any kind of propagation that enables other |
||
| 182 |
parties to make or receive copies. Mere interaction with a user through |
||
| 183 |
a computer network, with no transfer of a copy, is not conveying. |
||
| 184 | |||
| 185 |
An interactive user interface displays "Appropriate Legal Notices" |
||
| 186 |
to the extent that it includes a convenient and prominently visible |
||
| 187 |
feature that (1) displays an appropriate copyright notice, and (2) |
||
| 188 |
tells the user that there is no warranty for the work (except to the |
||
| 189 |
extent that warranties are provided), that licensees may convey the |
||
| 190 |
work under this License, and how to view a copy of this License. If |
||
| 191 |
the interface presents a list of user commands or options, such as a |
||
| 192 |
menu, a prominent item in the list meets this criterion. |
||
| 193 | |||
| 194 |
1. Source Code. |
||
| 195 | |||
| 196 |
The "source code" for a work means the preferred form of the work |
||
| 197 |
for making modifications to it. "Object code" means any non-source |
||
| 198 |
form of a work. |
||
| 199 | |||
| 200 |
A "Standard Interface" means an interface that either is an official |
||
| 201 |
standard defined by a recognized standards body, or, in the case of |
||
| 202 |
interfaces specified for a particular programming language, one that |
||
| 203 |
is widely used among developers working in that language. |
||
| 204 | |||
| 205 |
The "System Libraries" of an executable work include anything, other |
||
| 206 |
than the work as a whole, that (a) is included in the normal form of |
||
| 207 |
packaging a Major Component, but which is not part of that Major |
||
| 208 |
Component, and (b) serves only to enable use of the work with that |
||
| 209 |
Major Component, or to implement a Standard Interface for which an |
||
| 210 |
implementation is available to the public in source code form. A |
||
| 211 |
"Major Component", in this context, means a major essential component |
||
| 212 |
(kernel, window system, and so on) of the specific operating system |
||
| 213 |
(if any) on which the executable work runs, or a compiler used to |
||
| 214 |
produce the work, or an object code interpreter used to run it. |
||
| 215 | |||
| 216 |
The "Corresponding Source" for a work in object code form means all |
||
| 217 |
the source code needed to generate, install, and (for an executable |
||
| 218 |
work) run the object code and to modify the work, including scripts to |
||
| 219 |
control those activities. However, it does not include the work's |
||
| 220 |
System Libraries, or general-purpose tools or generally available free |
||
| 221 |
programs which are used unmodified in performing those activities but |
||
| 222 |
which are not part of the work. For example, Corresponding Source |
||
| 223 |
includes interface definition files associated with source files for |
||
| 224 |
the work, and the source code for shared libraries and dynamically |
||
| 225 |
linked subprograms that the work is specifically designed to require, |
||
| 226 |
such as by intimate data communication or control flow between those |
||
| 227 |
subprograms and other parts of the work. |
||
| 228 | |||
| 229 |
The Corresponding Source need not include anything that users |
||
| 230 |
can regenerate automatically from other parts of the Corresponding |
||
| 231 |
Source. |
||
| 232 | |||
| 233 |
The Corresponding Source for a work in source code form is that |
||
| 234 |
same work. |
||
| 235 | |||
| 236 |
2. Basic Permissions. |
||
| 237 | |||
| 238 |
All rights granted under this License are granted for the term of |
||
| 239 |
copyright on the Program, and are irrevocable provided the stated |
||
| 240 |
conditions are met. This License explicitly affirms your unlimited |
||
| 241 |
permission to run the unmodified Program. The output from running a |
||
| 242 |
covered work is covered by this License only if the output, given its |
||
| 243 |
content, constitutes a covered work. This License acknowledges your |
||
| 244 |
rights of fair use or other equivalent, as provided by copyright law. |
||
| 245 | |||
| 246 |
You may make, run and propagate covered works that you do not |
||
| 247 |
convey, without conditions so long as your license otherwise remains |
||
| 248 |
in force. You may convey covered works to others for the sole purpose |
||
| 249 |
of having them make modifications exclusively for you, or provide you |
||
| 250 |
with facilities for running those works, provided that you comply with |
||
| 251 |
the terms of this License in conveying all material for which you do |
||
| 252 |
not control copyright. Those thus making or running the covered works |
||
| 253 |
for you must do so exclusively on your behalf, under your direction |
||
| 254 |
and control, on terms that prohibit them from making any copies of |
||
| 255 |
your copyrighted material outside their relationship with you. |
||
| 256 | |||
| 257 |
Conveying under any other circumstances is permitted solely under |
||
| 258 |
the conditions stated below. Sublicensing is not allowed; section 10 |
||
| 259 |
makes it unnecessary. |
||
| 260 | |||
| 261 |
3. Protecting Users' Legal Rights From Anti-Circumvention Law. |
||
| 262 | |||
| 263 |
No covered work shall be deemed part of an effective technological |
||
| 264 |
measure under any applicable law fulfilling obligations under article |
||
| 265 |
11 of the WIPO copyright treaty adopted on 20 December 1996, or |
||
| 266 |
similar laws prohibiting or restricting circumvention of such |
||
| 267 |
measures. |
||
| 268 | |||
| 269 |
When you convey a covered work, you waive any legal power to forbid |
||
| 270 |
circumvention of technological measures to the extent such circumvention |
||
| 271 |
is effected by exercising rights under this License with respect to |
||
| 272 |
the covered work, and you disclaim any intention to limit operation or |
||
| 273 |
modification of the work as a means of enforcing, against the work's |
||
| 274 |
users, your or third parties' legal rights to forbid circumvention of |
||
| 275 |
technological measures. |
||
| 276 | |||
| 277 |
4. Conveying Verbatim Copies. |
||
| 278 | |||
| 279 |
You may convey verbatim copies of the Program's source code as you |
||
| 280 |
receive it, in any medium, provided that you conspicuously and |
||
| 281 |
appropriately publish on each copy an appropriate copyright notice; |
||
| 282 |
keep intact all notices stating that this License and any |
||
| 283 |
non-permissive terms added in accord with section 7 apply to the code; |
||
| 284 |
keep intact all notices of the absence of any warranty; and give all |
||
| 285 |
recipients a copy of this License along with the Program. |
||
| 286 | |||
| 287 |
You may charge any price or no price for each copy that you convey, |
||
| 288 |
and you may offer support or warranty protection for a fee. |
||
| 289 | |||
| 290 |
5. Conveying Modified Source Versions. |
||
| 291 | |||
| 292 |
You may convey a work based on the Program, or the modifications to |
||
| 293 |
produce it from the Program, in the form of source code under the |
||
| 294 |
terms of section 4, provided that you also meet all of these conditions: |
||
| 295 | |||
| 296 |
a) The work must carry prominent notices stating that you modified |
||
| 297 |
it, and giving a relevant date. |
||
| 298 | |||
| 299 |
b) The work must carry prominent notices stating that it is |
||
| 300 |
released under this License and any conditions added under section |
||
| 301 |
7. This requirement modifies the requirement in section 4 to |
||
| 302 |
"keep intact all notices". |
||
| 303 | |||
| 304 |
c) You must license the entire work, as a whole, under this |
||
| 305 |
License to anyone who comes into possession of a copy. This |
||
| 306 |
License will therefore apply, along with any applicable section 7 |
||
| 307 |
additional terms, to the whole of the work, and all its parts, |
||
| 308 |
regardless of how they are packaged. This License gives no |
||
| 309 |
permission to license the work in any other way, but it does not |
||
| 310 |
invalidate such permission if you have separately received it. |
||
| 311 | |||
| 312 |
d) If the work has interactive user interfaces, each must display |
||
| 313 |
Appropriate Legal Notices; however, if the Program has interactive |
||
| 314 |
interfaces that do not display Appropriate Legal Notices, your |
||
| 315 |
work need not make them do so. |
||
| 316 | |||
| 317 |
A compilation of a covered work with other separate and independent |
||
| 318 |
works, which are not by their nature extensions of the covered work, |
||
| 319 |
and which are not combined with it such as to form a larger program, |
||
| 320 |
in or on a volume of a storage or distribution medium, is called an |
||
| 321 |
"aggregate" if the compilation and its resulting copyright are not |
||
| 322 |
used to limit the access or legal rights of the compilation's users |
||
| 323 |
beyond what the individual works permit. Inclusion of a covered work |
||
| 324 |
in an aggregate does not cause this License to apply to the other |
||
| 325 |
parts of the aggregate. |
||
| 326 | |||
| 327 |
6. Conveying Non-Source Forms. |
||
| 328 | |||
| 329 |
You may convey a covered work in object code form under the terms |
||
| 330 |
of sections 4 and 5, provided that you also convey the |
||
| 331 |
machine-readable Corresponding Source under the terms of this License, |
||
| 332 |
in one of these ways: |
||
| 333 | |||
| 334 |
a) Convey the object code in, or embodied in, a physical product |
||
| 335 |
(including a physical distribution medium), accompanied by the |
||
| 336 |
Corresponding Source fixed on a durable physical medium |
||
| 337 |
customarily used for software interchange. |
||
| 338 | |||
| 339 |
b) Convey the object code in, or embodied in, a physical product |
||
| 340 |
(including a physical distribution medium), accompanied by a |
||
| 341 |
written offer, valid for at least three years and valid for as |
||
| 342 |
long as you offer spare parts or customer support for that product |
||
| 343 |
model, to give anyone who possesses the object code either (1) a |
||
| 344 |
copy of the Corresponding Source for all the software in the |
||
| 345 |
product that is covered by this License, on a durable physical |
||
| 346 |
medium customarily used for software interchange, for a price no |
||
| 347 |
more than your reasonable cost of physically performing this |
||
| 348 |
conveying of source, or (2) access to copy the |
||
| 349 |
Corresponding Source from a network server at no charge. |
||
| 350 | |||
| 351 |
c) Convey individual copies of the object code with a copy of the |
||
| 352 |
written offer to provide the Corresponding Source. This |
||
| 353 |
alternative is allowed only occasionally and noncommercially, and |
||
| 354 |
only if you received the object code with such an offer, in accord |
||
| 355 |
with subsection 6b. |
||
| 356 | |||
| 357 |
d) Convey the object code by offering access from a designated |
||
| 358 |
place (gratis or for a charge), and offer equivalent access to the |
||
| 359 |
Corresponding Source in the same way through the same place at no |
||
| 360 |
further charge. You need not require recipients to copy the |
||
| 361 |
Corresponding Source along with the object code. If the place to |
||
| 362 |
copy the object code is a network server, the Corresponding Source |
||
| 363 |
may be on a different server (operated by you or a third party) |
||
| 364 |
that supports equivalent copying facilities, provided you maintain |
||
| 365 |
clear directions next to the object code saying where to find the |
||
| 366 |
Corresponding Source. Regardless of what server hosts the |
||
| 367 |
Corresponding Source, you remain obligated to ensure that it is |
||
| 368 |
available for as long as needed to satisfy these requirements. |
||
| 369 | |||
| 370 |
e) Convey the object code using peer-to-peer transmission, provided |
||
| 371 |
you inform other peers where the object code and Corresponding |
||
| 372 |
Source of the work are being offered to the general public at no |
||
| 373 |
charge under subsection 6d. |
||
| 374 | |||
| 375 |
A separable portion of the object code, whose source code is excluded |
||
| 376 |
from the Corresponding Source as a System Library, need not be |
||
| 377 |
included in conveying the object code work. |
||
| 378 | |||
| 379 |
A "User Product" is either (1) a "consumer product", which means any |
||
| 380 |
tangible personal property which is normally used for personal, family, |
||
| 381 |
or household purposes, or (2) anything designed or sold for incorporation |
||
| 382 |
into a dwelling. In determining whether a product is a consumer product, |
||
| 383 |
doubtful cases shall be resolved in favor of coverage. For a particular |
||
| 384 |
product received by a particular user, "normally used" refers to a |
||
| 385 |
typical or common use of that class of product, regardless of the status |
||
| 386 |
of the particular user or of the way in which the particular user |
||
| 387 |
actually uses, or expects or is expected to use, the product. A product |
||
| 388 |
is a consumer product regardless of whether the product has substantial |
||
| 389 |
commercial, industrial or non-consumer uses, unless such uses represent |
||
| 390 |
the only significant mode of use of the product. |
||
| 391 | |||
| 392 |
"Installation Information" for a User Product means any methods, |
||
| 393 |
procedures, authorization keys, or other information required to install |
||
| 394 |
and execute modified versions of a covered work in that User Product from |
||
| 395 |
a modified version of its Corresponding Source. The information must |
||
| 396 |
suffice to ensure that the continued functioning of the modified object |
||
| 397 |
code is in no case prevented or interfered with solely because |
||
| 398 |
modification has been made. |
||
| 399 | |||
| 400 |
If you convey an object code work under this section in, or with, or |
||
| 401 |
specifically for use in, a User Product, and the conveying occurs as |
||
| 402 |
part of a transaction in which the right of possession and use of the |
||
| 403 |
User Product is transferred to the recipient in perpetuity or for a |
||
| 404 |
fixed term (regardless of how the transaction is characterized), the |
||
| 405 |
Corresponding Source conveyed under this section must be accompanied |
||
| 406 |
by the Installation Information. But this requirement does not apply |
||
| 407 |
if neither you nor any third party retains the ability to install |
||
| 408 |
modified object code on the User Product (for example, the work has |
||
| 409 |
been installed in ROM). |
||
| 410 | |||
| 411 |
The requirement to provide Installation Information does not include a |
||
| 412 |
requirement to continue to provide support service, warranty, or updates |
||
| 413 |
for a work that has been modified or installed by the recipient, or for |
||
| 414 |
the User Product in which it has been modified or installed. Access to a |
||
| 415 |
network may be denied when the modification itself materially and |
||
| 416 |
adversely affects the operation of the network or violates the rules and |
||
| 417 |
protocols for communication across the network. |
||
| 418 | |||
| 419 |
Corresponding Source conveyed, and Installation Information provided, |
||
| 420 |
in accord with this section must be in a format that is publicly |
||
| 421 |
documented (and with an implementation available to the public in |
||
| 422 |
source code form), and must require no special password or key for |
||
| 423 |
unpacking, reading or copying. |
||
| 424 | |||
| 425 |
7. Additional Terms. |
||
| 426 | |||
| 427 |
"Additional permissions" are terms that supplement the terms of this |
||
| 428 |
License by making exceptions from one or more of its conditions. |
||
| 429 |
Additional permissions that are applicable to the entire Program shall |
||
| 430 |
be treated as though they were included in this License, to the extent |
||
| 431 |
that they are valid under applicable law. If additional permissions |
||
| 432 |
apply only to part of the Program, that part may be used separately |
||
| 433 |
under those permissions, but the entire Program remains governed by |
||
| 434 |
this License without regard to the additional permissions. |
||
| 435 | |||
| 436 |
When you convey a copy of a covered work, you may at your option |
||
| 437 |
remove any additional permissions from that copy, or from any part of |
||
| 438 |
it. (Additional permissions may be written to require their own |
||
| 439 |
removal in certain cases when you modify the work.) You may place |
||
| 440 |
additional permissions on material, added by you to a covered work, |
||
| 441 |
for which you have or can give appropriate copyright permission. |
||
| 442 | |||
| 443 |
Notwithstanding any other provision of this License, for material you |
||
| 444 |
add to a covered work, you may (if authorized by the copyright holders of |
||
| 445 |
that material) supplement the terms of this License with terms: |
||
| 446 | |||
| 447 |
a) Disclaiming warranty or limiting liability differently from the |
||
| 448 |
terms of sections 15 and 16 of this License; or |
||
| 449 | |||
| 450 |
b) Requiring preservation of specified reasonable legal notices or |
||
| 451 |
author attributions in that material or in the Appropriate Legal |
||
| 452 |
Notices displayed by works containing it; or |
||
| 453 | |||
| 454 |
c) Prohibiting misrepresentation of the origin of that material, or |
||
| 455 |
requiring that modified versions of such material be marked in |
||
| 456 |
reasonable ways as different from the original version; or |
||
| 457 | |||
| 458 |
d) Limiting the use for publicity purposes of names of licensors or |
||
| 459 |
authors of the material; or |
||
| 460 | |||
| 461 |
e) Declining to grant rights under trademark law for use of some |
||
| 462 |
trade names, trademarks, or service marks; or |
||
| 463 | |||
| 464 |
f) Requiring indemnification of licensors and authors of that |
||
| 465 |
material by anyone who conveys the material (or modified versions of |
||
| 466 |
it) with contractual assumptions of liability to the recipient, for |
||
| 467 |
any liability that these contractual assumptions directly impose on |
||
| 468 |
those licensors and authors. |
||
| 469 | |||
| 470 |
All other non-permissive additional terms are considered "further |
||
| 471 |
restrictions" within the meaning of section 10. If the Program as you |
||
| 472 |
received it, or any part of it, contains a notice stating that it is |
||
| 473 |
governed by this License along with a term that is a further |
||
| 474 |
restriction, you may remove that term. If a license document contains |
||
| 475 |
a further restriction but permits relicensing or conveying under this |
||
| 476 |
License, you may add to a covered work material governed by the terms |
||
| 477 |
of that license document, provided that the further restriction does |
||
| 478 |
not survive such relicensing or conveying. |
||
| 479 | |||
| 480 |
If you add terms to a covered work in accord with this section, you |
||
| 481 |
must place, in the relevant source files, a statement of the |
||
| 482 |
additional terms that apply to those files, or a notice indicating |
||
| 483 |
where to find the applicable terms. |
||
| 484 | |||
| 485 |
Additional terms, permissive or non-permissive, may be stated in the |
||
| 486 |
form of a separately written license, or stated as exceptions; |
||
| 487 |
the above requirements apply either way. |
||
| 488 | |||
| 489 |
8. Termination. |
||
| 490 | |||
| 491 |
You may not propagate or modify a covered work except as expressly |
||
| 492 |
provided under this License. Any attempt otherwise to propagate or |
||
| 493 |
modify it is void, and will automatically terminate your rights under |
||
| 494 |
this License (including any patent licenses granted under the third |
||
| 495 |
paragraph of section 11). |
||
| 496 | |||
| 497 |
However, if you cease all violation of this License, then your |
||
| 498 |
license from a particular copyright holder is reinstated (a) |
||
| 499 |
provisionally, unless and until the copyright holder explicitly and |
||
| 500 |
finally terminates your license, and (b) permanently, if the copyright |
||
| 501 |
holder fails to notify you of the violation by some reasonable means |
||
| 502 |
prior to 60 days after the cessation. |
||
| 503 | |||
| 504 |
Moreover, your license from a particular copyright holder is |
||
| 505 |
reinstated permanently if the copyright holder notifies you of the |
||
| 506 |
violation by some reasonable means, this is the first time you have |
||
| 507 |
received notice of violation of this License (for any work) from that |
||
| 508 |
copyright holder, and you cure the violation prior to 30 days after |
||
| 509 |
your receipt of the notice. |
||
| 510 | |||
| 511 |
Termination of your rights under this section does not terminate the |
||
| 512 |
licenses of parties who have received copies or rights from you under |
||
| 513 |
this License. If your rights have been terminated and not permanently |
||
| 514 |
reinstated, you do not qualify to receive new licenses for the same |
||
| 515 |
material under section 10. |
||
| 516 | |||
| 517 |
9. Acceptance Not Required for Having Copies. |
||
| 518 | |||
| 519 |
You are not required to accept this License in order to receive or |
||
| 520 |
run a copy of the Program. Ancillary propagation of a covered work |
||
| 521 |
occurring solely as a consequence of using peer-to-peer transmission |
||
| 522 |
to receive a copy likewise does not require acceptance. However, |
||
| 523 |
nothing other than this License grants you permission to propagate or |
||
| 524 |
modify any covered work. These actions infringe copyright if you do |
||
| 525 |
not accept this License. Therefore, by modifying or propagating a |
||
| 526 |
covered work, you indicate your acceptance of this License to do so. |
||
| 527 | |||
| 528 |
10. Automatic Licensing of Downstream Recipients. |
||
| 529 | |||
| 530 |
Each time you convey a covered work, the recipient automatically |
||
| 531 |
receives a license from the original licensors, to run, modify and |
||
| 532 |
propagate that work, subject to this License. You are not responsible |
||
| 533 |
for enforcing compliance by third parties with this License. |
||
| 534 | |||
| 535 |
An "entity transaction" is a transaction transferring control of an |
||
| 536 |
organization, or substantially all assets of one, or subdividing an |
||
| 537 |
organization, or merging organizations. If propagation of a covered |
||
| 538 |
work results from an entity transaction, each party to that |
||
| 539 |
transaction who receives a copy of the work also receives whatever |
||
| 540 |
licenses to the work the party's predecessor in interest had or could |
||
| 541 |
give under the previous paragraph, plus a right to possession of the |
||
| 542 |
Corresponding Source of the work from the predecessor in interest, if |
||
| 543 |
the predecessor has it or can get it with reasonable efforts. |
||
| 544 | |||
| 545 |
You may not impose any further restrictions on the exercise of the |
||
| 546 |
rights granted or affirmed under this License. For example, you may |
||
| 547 |
not impose a license fee, royalty, or other charge for exercise of |
||
| 548 |
rights granted under this License, and you may not initiate litigation |
||
| 549 |
(including a cross-claim or counterclaim in a lawsuit) alleging that |
||
| 550 |
any patent claim is infringed by making, using, selling, offering for |
||
| 551 |
sale, or importing the Program or any portion of it. |
||
| 552 | |||
| 553 |
11. Patents. |
||
| 554 | |||
| 555 |
A "contributor" is a copyright holder who authorizes use under this |
||
| 556 |
License of the Program or a work on which the Program is based. The |
||
| 557 |
work thus licensed is called the contributor's "contributor version". |
||
| 558 | |||
| 559 |
A contributor's "essential patent claims" are all patent claims |
||
| 560 |
owned or controlled by the contributor, whether already acquired or |
||
| 561 |
hereafter acquired, that would be infringed by some manner, permitted |
||
| 562 |
by this License, of making, using, or selling its contributor version, |
||
| 563 |
but do not include claims that would be infringed only as a |
||
| 564 |
consequence of further modification of the contributor version. For |
||
| 565 |
purposes of this definition, "control" includes the right to grant |
||
| 566 |
patent sublicenses in a manner consistent with the requirements of |
||
| 567 |
this License. |
||
| 568 | |||
| 569 |
Each contributor grants you a non-exclusive, worldwide, royalty-free |
||
| 570 |
patent license under the contributor's essential patent claims, to |
||
| 571 |
make, use, sell, offer for sale, import and otherwise run, modify and |
||
| 572 |
propagate the contents of its contributor version. |
||
| 573 | |||
| 574 |
In the following three paragraphs, a "patent license" is any express |
||
| 575 |
agreement or commitment, however denominated, not to enforce a patent |
||
| 576 |
(such as an express permission to practice a patent or covenant not to |
||
| 577 |
sue for patent infringement). To "grant" such a patent license to a |
||
| 578 |
party means to make such an agreement or commitment not to enforce a |
||
| 579 |
patent against the party. |
||
| 580 | |||
| 581 |
If you convey a covered work, knowingly relying on a patent license, |
||
| 582 |
and the Corresponding Source of the work is not available for anyone |
||
| 583 |
to copy, free of charge and under the terms of this License, through a |
||
| 584 |
publicly available network server or other readily accessible means, |
||
| 585 |
then you must either (1) cause the Corresponding Source to be so |
||
| 586 |
available, or (2) arrange to deprive yourself of the benefit of the |
||
| 587 |
patent license for this particular work, or (3) arrange, in a manner |
||
| 588 |
consistent with the requirements of this License, to extend the patent |
||
| 589 |
license to downstream recipients. "Knowingly relying" means you have |
||
| 590 |
actual knowledge that, but for the patent license, your conveying the |
||
| 591 |
covered work in a country, or your recipient's use of the covered work |
||
| 592 |
in a country, would infringe one or more identifiable patents in that |
||
| 593 |
country that you have reason to believe are valid. |
||
| 594 | |||
| 595 |
If, pursuant to or in connection with a single transaction or |
||
| 596 |
arrangement, you convey, or propagate by procuring conveyance of, a |
||
| 597 |
covered work, and grant a patent license to some of the parties |
||
| 598 |
receiving the covered work authorizing them to use, propagate, modify |
||
| 599 |
or convey a specific copy of the covered work, then the patent license |
||
| 600 |
you grant is automatically extended to all recipients of the covered |
||
| 601 |
work and works based on it. |
||
| 602 | |||
| 603 |
A patent license is "discriminatory" if it does not include within |
||
| 604 |
the scope of its coverage, prohibits the exercise of, or is |
||
| 605 |
conditioned on the non-exercise of one or more of the rights that are |
||
| 606 |
specifically granted under this License. You may not convey a covered |
||
| 607 |
work if you are a party to an arrangement with a third party that is |
||
| 608 |
in the business of distributing software, under which you make payment |
||
| 609 |
to the third party based on the extent of your activity of conveying |
||
| 610 |
the work, and under which the third party grants, to any of the |
||
| 611 |
parties who would receive the covered work from you, a discriminatory |
||
| 612 |
patent license (a) in connection with copies of the covered work |
||
| 613 |
conveyed by you (or copies made from those copies), or (b) primarily |
||
| 614 |
for and in connection with specific products or compilations that |
||
| 615 |
contain the covered work, unless you entered into that arrangement, |
||
| 616 |
or that patent license was granted, prior to 28 March 2007. |
||
| 617 | |||
| 618 |
Nothing in this License shall be construed as excluding or limiting |
||
| 619 |
any implied license or other defenses to infringement that may |
||
| 620 |
otherwise be available to you under applicable patent law. |
||
| 621 | |||
| 622 |
12. No Surrender of Others' Freedom. |
||
| 623 | |||
| 624 |
If conditions are imposed on you (whether by court order, agreement or |
||
| 625 |
otherwise) that contradict the conditions of this License, they do not |
||
| 626 |
excuse you from the conditions of this License. If you cannot convey a |
||
| 627 |
covered work so as to satisfy simultaneously your obligations under this |
||
| 628 |
License and any other pertinent obligations, then as a consequence you may |
||
| 629 |
not convey it at all. For example, if you agree to terms that obligate you |
||
| 630 |
to collect a royalty for further conveying from those to whom you convey |
||
| 631 |
the Program, the only way you could satisfy both those terms and this |
||
| 632 |
License would be to refrain entirely from conveying the Program. |
||
| 633 | |||
| 634 |
13. Use with the GNU Affero General Public License. |
||
| 635 | |||
| 636 |
Notwithstanding any other provision of this License, you have |
||
| 637 |
permission to link or combine any covered work with a work licensed |
||
| 638 |
under version 3 of the GNU Affero General Public License into a single |
||
| 639 |
combined work, and to convey the resulting work. The terms of this |
||
| 640 |
License will continue to apply to the part which is the covered work, |
||
| 641 |
but the special requirements of the GNU Affero General Public License, |
||
| 642 |
section 13, concerning interaction through a network will apply to the |
||
| 643 |
combination as such. |
||
| 644 | |||
| 645 |
14. Revised Versions of this License. |
||
| 646 | |||
| 647 |
The Free Software Foundation may publish revised and/or new versions of |
||
| 648 |
the GNU General Public License from time to time. Such new versions will |
||
| 649 |
be similar in spirit to the present version, but may differ in detail to |
||
| 650 |
address new problems or concerns. |
||
| 651 | |||
| 652 |
Each version is given a distinguishing version number. If the |
||
| 653 |
Program specifies that a certain numbered version of the GNU General |
||
| 654 |
Public License "or any later version" applies to it, you have the |
||
| 655 |
option of following the terms and conditions either of that numbered |
||
| 656 |
version or of any later version published by the Free Software |
||
| 657 |
Foundation. If the Program does not specify a version number of the |
||
| 658 |
GNU General Public License, you may choose any version ever published |
||
| 659 |
by the Free Software Foundation. |
||
| 660 | |||
| 661 |
If the Program specifies that a proxy can decide which future |
||
| 662 |
versions of the GNU General Public License can be used, that proxy's |
||
| 663 |
public statement of acceptance of a version permanently authorizes you |
||
| 664 |
to choose that version for the Program. |
||
| 665 | |||
| 666 |
Later license versions may give you additional or different |
||
| 667 |
permissions. However, no additional obligations are imposed on any |
||
| 668 |
author or copyright holder as a result of your choosing to follow a |
||
| 669 |
later version. |
||
| 670 | |||
| 671 |
15. Disclaimer of Warranty. |
||
| 672 | |||
| 673 |
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY |
||
| 674 |
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT |
||
| 675 |
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY |
||
| 676 |
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, |
||
| 677 |
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||
| 678 |
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM |
||
| 679 |
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF |
||
| 680 |
ALL NECESSARY SERVICING, REPAIR OR CORRECTION. |
||
| 681 | |||
| 682 |
16. Limitation of Liability. |
||
| 683 | |||
| 684 |
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
||
| 685 |
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS |
||
| 686 |
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY |
||
| 687 |
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE |
||
| 688 |
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF |
||
| 689 |
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD |
||
| 690 |
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), |
||
| 691 |
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF |
||
| 692 |
SUCH DAMAGES. |
||
| 693 | |||
| 694 |
17. Interpretation of Sections 15 and 16. |
||
| 695 | |||
| 696 |
If the disclaimer of warranty and limitation of liability provided |
||
| 697 |
above cannot be given local legal effect according to their terms, |
||
| 698 |
reviewing courts shall apply local law that most closely approximates |
||
| 699 |
an absolute waiver of all civil liability in connection with the |
||
| 700 |
Program, unless a warranty or assumption of liability accompanies a |
||
| 701 |
copy of the Program in return for a fee. |
||
| 702 | |||
| 703 |
END OF TERMS AND CONDITIONS |
||
| 704 | |||
| 705 |
How to Apply These Terms to Your New Programs |
||
| 706 | |||
| 707 |
If you develop a new program, and you want it to be of the greatest |
||
| 708 |
possible use to the public, the best way to achieve this is to make it |
||
| 709 |
free software which everyone can redistribute and change under these terms. |
||
| 710 | |||
| 711 |
To do so, attach the following notices to the program. It is safest |
||
| 712 |
to attach them to the start of each source file to most effectively |
||
| 713 |
state the exclusion of warranty; and each file should have at least |
||
| 714 |
the "copyright" line and a pointer to where the full notice is found. |
||
| 715 | |||
| 716 |
<one line to give the program's name and a brief idea of what it does.> |
||
| 717 |
Copyright (C) <year> <name of author> |
||
| 718 | |||
| 719 |
This program is free software: you can redistribute it and/or modify |
||
| 720 |
it under the terms of the GNU General Public License as published by |
||
| 721 |
the Free Software Foundation, either version 3 of the License, or |
||
| 722 |
(at your option) any later version. |
||
| 723 | |||
| 724 |
This program is distributed in the hope that it will be useful, |
||
| 725 |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 726 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 727 |
GNU General Public License for more details. |
||
| 728 | |||
| 729 |
You should have received a copy of the GNU General Public License |
||
| 730 |
along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
| 731 | |||
| 732 |
Also add information on how to contact you by electronic and paper mail. |
||
| 733 | |||
| 734 |
If the program does terminal interaction, make it output a short |
||
| 735 |
notice like this when it starts in an interactive mode: |
||
| 736 | |||
| 737 |
<program> Copyright (C) <year> <name of author> |
||
| 738 |
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. |
||
| 739 |
This is free software, and you are welcome to redistribute it |
||
| 740 |
under certain conditions; type `show c' for details. |
||
| 741 | |||
| 742 |
The hypothetical commands `show w' and `show c' should show the appropriate |
||
| 743 |
parts of the General Public License. Of course, your program's commands |
||
| 744 |
might be different; for a GUI interface, you would use an "about box". |
||
| 745 | |||
| 746 |
You should also get your employer (if you work as a programmer) or school, |
||
| 747 |
if any, to sign a "copyright disclaimer" for the program, if necessary. |
||
| 748 |
For more information on this, and how to apply and follow the GNU GPL, see |
||
| 749 |
<http://www.gnu.org/licenses/>. |
||
| 750 | |||
| 751 |
The GNU General Public License does not permit incorporating your program |
||
| 752 |
into proprietary programs. If your program is a subroutine library, you |
||
| 753 |
may consider it more useful to permit linking proprietary applications with |
||
| 754 |
the library. If this is what you want to do, use the GNU Lesser General |
||
| 755 |
Public License instead of this License. But first, please read |
||
| 756 |
<http://www.gnu.org/philosophy/why-not-lgpl.html>. |
||
| 757 | |||
| 758 |
*/ |
||
| 759 |
| Lines covered: | 8 / 23 (34.8%) |
|---|
| 1 |
pragma solidity 0.8.17; |
||
| 2 | |||
| 3 |
import {IERC3156FlashBorrower} from "../../Interfaces/IERC3156FlashBorrower.sol";
|
||
| 4 |
import {IERC20} from "../../Dependencies/IERC20.sol";
|
||
| 5 | |||
| 6 |
contract Actor is IERC3156FlashBorrower {
|
||
| 7 |
address[] internal tokens; |
||
| 8 |
address[] internal callers; |
||
| 9 | |||
| 10 |
constructor(address[] memory _tokens, address[] memory _callers) payable {
|
||
| 11 |
√
|
tokens = _tokens; |
|
| 12 |
√
|
callers = _callers; |
|
| 13 |
√
|
for (uint256 i = 0; i < tokens.length; i++) {
|
|
| 14 |
√
|
IERC20(tokens[i]).approve(callers[i], type(uint256).max); |
|
| 15 |
} |
||
| 16 |
} |
||
| 17 | |||
| 18 |
function proxy( |
||
| 19 |
address _target, |
||
| 20 |
bytes memory _calldata |
||
| 21 |
√
|
⟳
|
) public returns (bool success, bytes memory returnData) {
|
| 22 |
√
|
⟳
|
(success, returnData) = address(_target).call(_calldata); |
| 23 |
} |
||
| 24 | |||
| 25 |
function proxy( |
||
| 26 |
address _target, |
||
| 27 |
bytes memory _calldata, |
||
| 28 |
uint256 value |
||
| 29 |
√
|
) public returns (bool success, bytes memory returnData) {
|
|
| 30 |
√
|
(success, returnData) = address(_target).call{value: value}(_calldata);
|
|
| 31 |
} |
||
| 32 | |||
| 33 |
receive() external payable {}
|
||
| 34 | |||
| 35 |
// callback for flashloan |
||
| 36 |
function onFlashLoan( |
||
| 37 |
address, |
||
| 38 |
address token, |
||
| 39 |
uint256 amount, |
||
| 40 |
uint256 fee, |
||
| 41 |
bytes calldata data |
||
| 42 |
) external override returns (bytes32) {
|
||
| 43 |
bool isValidCaller = false; |
||
| 44 |
for (uint256 i = 0; i < tokens.length; i++) {
|
||
| 45 |
if (token == tokens[i]) {
|
||
| 46 |
isValidCaller = msg.sender == callers[i]; |
||
| 47 |
break; |
||
| 48 |
} |
||
| 49 |
} |
||
| 50 |
require(isValidCaller, "Invalid caller"); |
||
| 51 | |||
| 52 |
if (data.length != 0) {
|
||
| 53 |
(address[] memory _targets, bytes[] memory _calldatas) = abi.decode( |
||
| 54 |
data, |
||
| 55 |
(address[], bytes[]) |
||
| 56 |
); |
||
| 57 |
for (uint256 i = 0; i < _targets.length; ++i) {
|
||
| 58 |
(bool success, ) = address(_targets[i]).call(_calldatas[i]); |
||
| 59 |
require(success); |
||
| 60 |
} |
||
| 61 |
} |
||
| 62 | |||
| 63 |
IERC20(token).approve(msg.sender, amount + fee); |
||
| 64 | |||
| 65 |
return keccak256("ERC3156FlashBorrower.onFlashLoan");
|
||
| 66 |
} |
||
| 67 |
} |
||
| 68 |
| Lines covered: | 20 / 31 (64.5%) |
|---|
| 1 |
pragma solidity 0.8.17; |
||
| 2 | |||
| 3 |
abstract contract Asserts {
|
||
| 4 |
event L1(uint256); |
||
| 5 |
event L2(uint256, uint256); |
||
| 6 |
event L3(uint256, uint256, uint256); |
||
| 7 |
event L4(uint256, uint256, uint256, uint256); |
||
| 8 | |||
| 9 |
function gt(uint256 a, uint256 b, string memory reason) internal virtual; |
||
| 10 | |||
| 11 |
function gte(uint256 a, uint256 b, string memory reason) internal virtual; |
||
| 12 | |||
| 13 |
function lt(uint256 a, uint256 b, string memory reason) internal virtual; |
||
| 14 | |||
| 15 |
function lte(uint256 a, uint256 b, string memory reason) internal virtual; |
||
| 16 | |||
| 17 |
function eq(uint256 a, uint256 b, string memory reason) internal virtual; |
||
| 18 | |||
| 19 |
function t(bool b, string memory reason) internal virtual; |
||
| 20 | |||
| 21 |
function between(uint256 value, uint256 low, uint256 high) internal virtual returns (uint256); |
||
| 22 | |||
| 23 |
function isApproximateEq( |
||
| 24 |
uint256 _num1, |
||
| 25 |
uint256 _num2, |
||
| 26 |
uint256 _tolerance |
||
| 27 |
√
|
) internal pure returns (bool) {
|
|
| 28 |
√
|
return diffPercent(_num1, _num2) <= _tolerance; |
|
| 29 |
} |
||
| 30 | |||
| 31 |
√
|
function diffPercent(uint256 _num1, uint256 _num2) internal pure returns (uint256) {
|
|
| 32 |
√
|
if (_num1 == _num2) return 0; |
|
| 33 |
√
|
else if (_num1 > _num2) {
|
|
| 34 |
return ((_num1 - _num2) * 1e18) / ((_num1 + _num2) / 2); |
||
| 35 |
} else {
|
||
| 36 |
√
|
return ((_num2 - _num1) * 1e18) / ((_num1 + _num2) / 2); |
|
| 37 |
} |
||
| 38 |
} |
||
| 39 | |||
| 40 |
// https://ethereum.stackexchange.com/a/83577 |
||
| 41 |
√
|
function _getRevertMsg(bytes memory returnData) internal pure returns (string memory) {
|
|
| 42 |
// Check that the data has the right size: 4 bytes for signature + 32 bytes for panic code |
||
| 43 |
√
|
if (returnData.length == 4 + 32) {
|
|
| 44 |
// Check that the data starts with the Panic signature |
||
| 45 |
bytes4 panicSignature = bytes4(keccak256(bytes("Panic(uint256)")));
|
||
| 46 |
for (uint i = 0; i < 4; i++) {
|
||
| 47 |
if (returnData[i] != panicSignature[i]) return "Undefined signature"; |
||
| 48 |
} |
||
| 49 | |||
| 50 |
uint256 panicCode; |
||
| 51 |
for (uint i = 4; i < 36; i++) {
|
||
| 52 |
panicCode = panicCode << 8; |
||
| 53 |
panicCode |= uint8(returnData[i]); |
||
| 54 |
} |
||
| 55 | |||
| 56 |
// Now convert the panic code into its string representation |
||
| 57 |
if (panicCode == 17) {
|
||
| 58 |
return "Panic(17)"; |
||
| 59 |
} |
||
| 60 | |||
| 61 |
// Add other panic codes as needed or return a generic "Unknown panic" |
||
| 62 |
return "Undefined panic code"; |
||
| 63 |
} |
||
| 64 | |||
| 65 |
// If the returnData length is less than 68, then the transaction failed silently (without a revert message) |
||
| 66 |
√
|
if (returnData.length < 68) return "Transaction reverted silently"; |
|
| 67 | |||
| 68 |
assembly {
|
||
| 69 |
// Slice the sighash. |
||
| 70 |
√
|
returnData := add(returnData, 0x04) |
|
| 71 |
} |
||
| 72 |
√
|
return abi.decode(returnData, (string)); // All that remains is the revert string |
|
| 73 |
} |
||
| 74 | |||
| 75 |
function _isRevertReasonEqual( |
||
| 76 |
bytes memory returnData, |
||
| 77 |
string memory reason |
||
| 78 |
√
|
) internal pure returns (bool) {
|
|
| 79 |
√
|
return (keccak256(abi.encodePacked(_getRevertMsg(returnData))) == |
|
| 80 |
√
|
keccak256(abi.encodePacked(reason))); |
|
| 81 |
} |
||
| 82 | |||
| 83 |
√
|
⟳
|
function max(uint256 a, uint256 b) internal pure returns (uint256) {
|
| 84 |
√
|
⟳
|
return a >= b ? a : b; |
| 85 |
} |
||
| 86 | |||
| 87 |
√
|
⟳
|
function min(uint256 a, uint256 b) internal pure returns (uint256) {
|
| 88 |
√
|
⟳
|
return a < b ? a : b; |
| 89 |
} |
||
| 90 | |||
| 91 |
function assertRevertReasonNotEqual(bytes memory returnData, string memory reason) internal {
|
||
| 92 |
√
|
bool isEqual = _isRevertReasonEqual(returnData, reason); |
|
| 93 |
√
|
t(!isEqual, reason); |
|
| 94 |
} |
||
| 95 | |||
| 96 |
function assertRevertReasonEqual(bytes memory returnData, string memory reason) internal {
|
||
| 97 |
bool isEqual = _isRevertReasonEqual(returnData, reason); |
||
| 98 |
t(isEqual, reason); |
||
| 99 |
} |
||
| 100 | |||
| 101 |
function assertRevertReasonEqual( |
||
| 102 |
bytes memory returnData, |
||
| 103 |
string memory reason1, |
||
| 104 |
string memory reason2 |
||
| 105 |
) internal {
|
||
| 106 |
bool isEqual = _isRevertReasonEqual(returnData, reason1) || |
||
| 107 |
_isRevertReasonEqual(returnData, reason2); |
||
| 108 |
t(isEqual, string.concat(reason1, " OR ", reason2)); |
||
| 109 |
} |
||
| 110 | |||
| 111 |
function assertRevertReasonEqual( |
||
| 112 |
bytes memory returnData, |
||
| 113 |
string memory reason1, |
||
| 114 |
string memory reason2, |
||
| 115 |
string memory reason3 |
||
| 116 |
) internal {
|
||
| 117 |
bool isEqual = _isRevertReasonEqual(returnData, reason1) || |
||
| 118 |
_isRevertReasonEqual(returnData, reason2) || |
||
| 119 |
_isRevertReasonEqual(returnData, reason3); |
||
| 120 |
t(isEqual, string.concat(reason1, " OR ", reason2, " OR ", reason3)); |
||
| 121 |
} |
||
| 122 | |||
| 123 |
function assertRevertReasonEqual( |
||
| 124 |
bytes memory returnData, |
||
| 125 |
string memory reason1, |
||
| 126 |
string memory reason2, |
||
| 127 |
string memory reason3, |
||
| 128 |
string memory reason4 |
||
| 129 |
) internal {
|
||
| 130 |
bool isEqual = _isRevertReasonEqual(returnData, reason1) || |
||
| 131 |
_isRevertReasonEqual(returnData, reason2) || |
||
| 132 |
_isRevertReasonEqual(returnData, reason3) || |
||
| 133 |
_isRevertReasonEqual(returnData, reason4); |
||
| 134 |
t(isEqual, string.concat(reason1, " OR ", reason2, " OR ", reason3, " OR ", reason4)); |
||
| 135 |
} |
||
| 136 |
} |
||
| 137 |
| Lines covered: | 90 / 90 (100.0%) |
|---|
| 1 |
pragma solidity 0.8.17; |
||
| 2 | |||
| 3 |
import {Pretty, Strings} from "../Pretty.sol";
|
||
| 4 |
import {BaseStorageVariables} from "../BaseStorageVariables.sol";
|
||
| 5 | |||
| 6 |
abstract contract BeforeAfter is BaseStorageVariables {
|
||
| 7 |
using Strings for string; |
||
| 8 |
using Pretty for uint256; |
||
| 9 |
using Pretty for int256; |
||
| 10 |
using Pretty for bool; |
||
| 11 | |||
| 12 |
struct Vars {
|
||
| 13 |
uint256 valueInSystemBefore; |
||
| 14 |
uint256 valueInSystemAfter; |
||
| 15 |
uint256 nicrBefore; |
||
| 16 |
uint256 nicrAfter; |
||
| 17 |
uint256 icrBefore; |
||
| 18 |
uint256 icrAfter; |
||
| 19 |
uint256 newIcrBefore; |
||
| 20 |
uint256 newIcrAfter; |
||
| 21 |
uint256 feeSplitBefore; |
||
| 22 |
uint256 feeSplitAfter; |
||
| 23 |
uint256 feeRecipientTotalCollBefore; |
||
| 24 |
uint256 feeRecipientTotalCollAfter; |
||
| 25 |
uint256 feeRecipientCollSharesBefore; |
||
| 26 |
uint256 feeRecipientCollSharesAfter; |
||
| 27 |
uint256 actorCollBefore; |
||
| 28 |
uint256 actorCollAfter; |
||
| 29 |
uint256 actorEbtcBefore; |
||
| 30 |
uint256 actorEbtcAfter; |
||
| 31 |
uint256 actorCdpCountBefore; |
||
| 32 |
uint256 actorCdpCountAfter; |
||
| 33 |
uint256 cdpCollBefore; |
||
| 34 |
uint256 cdpCollAfter; |
||
| 35 |
uint256 cdpDebtBefore; |
||
| 36 |
uint256 cdpDebtAfter; |
||
| 37 |
uint256 liquidatorRewardSharesBefore; |
||
| 38 |
uint256 liquidatorRewardSharesAfter; |
||
| 39 |
uint256 sortedCdpsSizeBefore; |
||
| 40 |
uint256 sortedCdpsSizeAfter; |
||
| 41 |
uint256 cdpStatusBefore; |
||
| 42 |
uint256 cdpStatusAfter; |
||
| 43 |
uint256 tcrBefore; |
||
| 44 |
uint256 tcrAfter; |
||
| 45 |
uint256 newTcrBefore; |
||
| 46 |
uint256 newTcrAfter; |
||
| 47 |
uint256 ebtcTotalSupplyBefore; |
||
| 48 |
uint256 ebtcTotalSupplyAfter; |
||
| 49 |
uint256 ethPerShareBefore; |
||
| 50 |
uint256 ethPerShareAfter; |
||
| 51 |
uint256 activePoolCollBefore; |
||
| 52 |
uint256 activePoolCollAfter; |
||
| 53 |
uint256 activePoolDebtBefore; |
||
| 54 |
uint256 activePoolDebtAfter; |
||
| 55 |
uint256 collSurplusPoolBefore; |
||
| 56 |
uint256 collSurplusPoolAfter; |
||
| 57 |
uint256 priceBefore; |
||
| 58 |
uint256 priceAfter; |
||
| 59 |
bool isRecoveryModeBefore; |
||
| 60 |
bool isRecoveryModeAfter; |
||
| 61 |
uint256 lastGracePeriodStartTimestampBefore; |
||
| 62 |
uint256 lastGracePeriodStartTimestampAfter; |
||
| 63 |
bool lastGracePeriodStartTimestampIsSetBefore; |
||
| 64 |
bool lastGracePeriodStartTimestampIsSetAfter; |
||
| 65 |
bool hasGracePeriodPassedBefore; |
||
| 66 |
bool hasGracePeriodPassedAfter; |
||
| 67 |
uint256 systemDebtRedistributionIndexBefore; |
||
| 68 |
uint256 systemDebtRedistributionIndexAfter; |
||
| 69 |
} |
||
| 70 | |||
| 71 |
Vars vars; |
||
| 72 |
struct Cdp {
|
||
| 73 |
bytes32 id; |
||
| 74 |
uint256 icr; |
||
| 75 |
} |
||
| 76 | |||
| 77 |
function _before(bytes32 _cdpId) internal {
|
||
| 78 |
√
|
⟳
|
vars.priceBefore = priceFeedMock.fetchPrice(); |
| 79 | |||
| 80 |
√
|
⟳
|
(uint256 debtBefore, , ) = cdpManager.getDebtAndCollShares(_cdpId); |
| 81 | |||
| 82 |
√
|
⟳
|
vars.nicrBefore = _cdpId != bytes32(0) ? crLens.quoteRealNICR(_cdpId) : 0; |
| 83 |
√
|
⟳
|
vars.icrBefore = _cdpId != bytes32(0) ? cdpManager.getICR(_cdpId, vars.priceBefore) : 0; |
| 84 |
√
|
⟳
|
vars.cdpCollBefore = _cdpId != bytes32(0) ? cdpManager.getCdpCollShares(_cdpId) : 0; |
| 85 |
√
|
⟳
|
vars.cdpDebtBefore = _cdpId != bytes32(0) ? debtBefore : 0; |
| 86 |
√
|
⟳
|
vars.liquidatorRewardSharesBefore = _cdpId != bytes32(0) |
| 87 |
√
|
⟳
|
? cdpManager.getCdpLiquidatorRewardShares(_cdpId) |
| 88 |
√
|
⟳
|
: 0; |
| 89 |
√
|
⟳
|
vars.cdpStatusBefore = _cdpId != bytes32(0) ? cdpManager.getCdpStatus(_cdpId) : 0; |
| 90 | |||
| 91 |
√
|
⟳
|
vars.isRecoveryModeBefore = crLens.quoteCheckRecoveryMode() == 1; /// @audit crLens |
| 92 |
√
|
⟳
|
(vars.feeSplitBefore, , ) = collateral.getPooledEthByShares(cdpManager.DECIMAL_PRECISION()) > |
| 93 |
√
|
⟳
|
cdpManager.stEthIndex() |
| 94 |
√
|
⟳
|
? cdpManager.calcFeeUponStakingReward( |
| 95 |
√
|
⟳
|
collateral.getPooledEthByShares(cdpManager.DECIMAL_PRECISION()), |
| 96 |
√
|
⟳
|
cdpManager.stEthIndex() |
| 97 |
) |
||
| 98 |
√
|
⟳
|
: (0, 0, 0); |
| 99 |
√
|
⟳
|
vars.feeRecipientTotalCollBefore = |
| 100 |
√
|
⟳
|
activePool.getFeeRecipientClaimableCollShares() + |
| 101 |
√
|
⟳
|
collateral.balanceOf(activePool.feeRecipientAddress()); |
| 102 |
√
|
⟳
|
vars.feeRecipientCollSharesBefore = activePool.getFeeRecipientClaimableCollShares(); |
| 103 |
√
|
⟳
|
vars.actorCollBefore = collateral.balanceOf(address(actor)); |
| 104 |
√
|
⟳
|
vars.actorEbtcBefore = eBTCToken.balanceOf(address(actor)); |
| 105 |
√
|
⟳
|
vars.actorCdpCountBefore = sortedCdps.cdpCountOf(address(actor)); |
| 106 |
√
|
⟳
|
vars.sortedCdpsSizeBefore = sortedCdps.getSize(); |
| 107 |
√
|
⟳
|
vars.tcrBefore = cdpManager.getTCR(vars.priceBefore); |
| 108 |
√
|
⟳
|
vars.ebtcTotalSupplyBefore = eBTCToken.totalSupply(); |
| 109 |
√
|
⟳
|
vars.ethPerShareBefore = collateral.getEthPerShare(); |
| 110 |
√
|
⟳
|
vars.activePoolDebtBefore = activePool.getSystemDebt(); |
| 111 |
√
|
⟳
|
vars.activePoolCollBefore = activePool.getSystemCollShares(); |
| 112 |
√
|
⟳
|
vars.collSurplusPoolBefore = collSurplusPool.getTotalSurplusCollShares(); |
| 113 |
√
|
⟳
|
vars.lastGracePeriodStartTimestampBefore = cdpManager.lastGracePeriodStartTimestamp(); |
| 114 |
√
|
⟳
|
vars.lastGracePeriodStartTimestampIsSetBefore = |
| 115 |
√
|
⟳
|
cdpManager.lastGracePeriodStartTimestamp() != cdpManager.UNSET_TIMESTAMP(); |
| 116 |
√
|
⟳
|
vars.hasGracePeriodPassedBefore = |
| 117 |
√
|
⟳
|
cdpManager.lastGracePeriodStartTimestamp() != cdpManager.UNSET_TIMESTAMP() && |
| 118 |
⟳
|
block.timestamp > |
|
| 119 |
⟳
|
cdpManager.lastGracePeriodStartTimestamp() + |
|
| 120 |
⟳
|
cdpManager.recoveryModeGracePeriodDuration(); |
|
| 121 |
√
|
⟳
|
vars.systemDebtRedistributionIndexBefore = cdpManager.systemDebtRedistributionIndex(); |
| 122 |
// TODO: Cleanup new vs old |
||
| 123 |
√
|
⟳
|
vars.newTcrBefore = crLens.quoteRealTCR(); |
| 124 |
√
|
⟳
|
vars.newIcrBefore = crLens.quoteRealICR(_cdpId); |
| 125 | |||
| 126 |
√
|
⟳
|
vars.valueInSystemBefore == (collateral.getPooledEthByShares |
| 127 |
√
|
⟳
|
(vars.activePoolCollBefore + |
| 128 |
√
|
⟳
|
vars.collSurplusPoolBefore + |
| 129 |
√
|
⟳
|
vars.feeRecipientTotalCollBefore) * vars.priceBefore) / |
| 130 |
√
|
⟳
|
1e18 - |
| 131 |
√
|
⟳
|
vars.activePoolDebtBefore; |
| 132 |
} |
||
| 133 | |||
| 134 |
function _after(bytes32 _cdpId) internal {
|
||
| 135 |
√
|
⟳
|
vars.priceAfter = priceFeedMock.fetchPrice(); |
| 136 | |||
| 137 |
√
|
⟳
|
vars.nicrAfter = _cdpId != bytes32(0) ? crLens.quoteRealNICR(_cdpId) : 0; |
| 138 |
√
|
⟳
|
vars.icrAfter = _cdpId != bytes32(0) ? cdpManager.getICR(_cdpId, vars.priceAfter) : 0; |
| 139 |
√
|
⟳
|
vars.cdpCollAfter = _cdpId != bytes32(0) ? cdpManager.getCdpCollShares(_cdpId) : 0; |
| 140 |
√
|
⟳
|
vars.cdpDebtAfter = _cdpId != bytes32(0) ? cdpManager.getCdpDebt(_cdpId) : 0; |
| 141 |
√
|
⟳
|
vars.liquidatorRewardSharesAfter = _cdpId != bytes32(0) |
| 142 |
√
|
⟳
|
? cdpManager.getCdpLiquidatorRewardShares(_cdpId) |
| 143 |
√
|
⟳
|
: 0; |
| 144 |
√
|
⟳
|
vars.cdpStatusAfter = _cdpId != bytes32(0) ? cdpManager.getCdpStatus(_cdpId) : 0; |
| 145 | |||
| 146 |
√
|
⟳
|
vars.isRecoveryModeAfter = cdpManager.checkRecoveryMode(vars.priceAfter); /// @audit This is fine as is because after the system is synched |
| 147 |
√
|
⟳
|
(vars.feeSplitAfter, , ) = collateral.getPooledEthByShares(cdpManager.DECIMAL_PRECISION()) > |
| 148 |
√
|
⟳
|
cdpManager.stEthIndex() |
| 149 |
√
|
? cdpManager.calcFeeUponStakingReward( |
|
| 150 |
√
|
collateral.getPooledEthByShares(cdpManager.DECIMAL_PRECISION()), |
|
| 151 |
√
|
cdpManager.stEthIndex() |
|
| 152 |
) |
||
| 153 |
√
|
⟳
|
: (0, 0, 0); |
| 154 | |||
| 155 |
√
|
⟳
|
vars.feeRecipientTotalCollAfter = |
| 156 |
√
|
⟳
|
activePool.getFeeRecipientClaimableCollShares() + |
| 157 |
√
|
⟳
|
collateral.sharesOf(activePool.feeRecipientAddress()); |
| 158 |
√
|
⟳
|
vars.feeRecipientCollSharesAfter = activePool.getFeeRecipientClaimableCollShares(); |
| 159 |
√
|
⟳
|
vars.actorCollAfter = collateral.balanceOf(address(actor)); |
| 160 |
√
|
⟳
|
vars.actorEbtcAfter = eBTCToken.balanceOf(address(actor)); |
| 161 |
√
|
⟳
|
vars.actorCdpCountAfter = sortedCdps.cdpCountOf(address(actor)); |
| 162 |
√
|
⟳
|
vars.sortedCdpsSizeAfter = sortedCdps.getSize(); |
| 163 |
√
|
⟳
|
vars.tcrAfter = cdpManager.getTCR(vars.priceAfter); |
| 164 |
√
|
⟳
|
vars.ebtcTotalSupplyAfter = eBTCToken.totalSupply(); |
| 165 |
√
|
⟳
|
vars.ethPerShareAfter = collateral.getEthPerShare(); |
| 166 |
√
|
⟳
|
vars.activePoolDebtAfter = activePool.getSystemDebt(); |
| 167 |
√
|
⟳
|
vars.activePoolCollAfter = activePool.getSystemCollShares(); |
| 168 |
√
|
⟳
|
vars.collSurplusPoolAfter = collSurplusPool.getTotalSurplusCollShares(); |
| 169 |
√
|
⟳
|
vars.lastGracePeriodStartTimestampAfter = cdpManager.lastGracePeriodStartTimestamp(); |
| 170 |
√
|
⟳
|
vars.lastGracePeriodStartTimestampIsSetAfter = |
| 171 |
√
|
⟳
|
cdpManager.lastGracePeriodStartTimestamp() != cdpManager.UNSET_TIMESTAMP(); |
| 172 |
√
|
⟳
|
vars.hasGracePeriodPassedAfter = |
| 173 |
√
|
⟳
|
cdpManager.lastGracePeriodStartTimestamp() != cdpManager.UNSET_TIMESTAMP() && |
| 174 |
√
|
⟳
|
block.timestamp > |
| 175 |
√
|
⟳
|
cdpManager.lastGracePeriodStartTimestamp() + |
| 176 |
√
|
⟳
|
cdpManager.recoveryModeGracePeriodDuration(); |
| 177 |
√
|
⟳
|
vars.systemDebtRedistributionIndexAfter = cdpManager.systemDebtRedistributionIndex(); |
| 178 | |||
| 179 |
√
|
⟳
|
vars.newTcrAfter = crLens.quoteRealTCR(); |
| 180 |
√
|
⟳
|
vars.newIcrAfter = crLens.quoteRealICR(_cdpId); |
| 181 | |||
| 182 |
// Value in system after |
||
| 183 |
√
|
⟳
|
vars.valueInSystemAfter = (collateral.getPooledEthByShares(vars.activePoolCollAfter + vars.collSurplusPoolAfter + vars.feeRecipientTotalCollAfter) * vars.priceAfter) / 1e18 - vars.activePoolDebtAfter; |
| 184 |
} |
||
| 185 | |||
| 186 |
function _diff() internal view returns (string memory log) {
|
||
| 187 |
log = string("\n\t\t\t\tBefore\t\t\tAfter\n");
|
||
| 188 |
if (vars.activePoolCollBefore != vars.activePoolCollAfter) {
|
||
| 189 |
log = log |
||
| 190 |
.concat("activePoolColl\t\t\t")
|
||
| 191 |
.concat(vars.activePoolCollBefore.pretty()) |
||
| 192 |
.concat("\t")
|
||
| 193 |
.concat(vars.activePoolCollAfter.pretty()) |
||
| 194 |
.concat("\n");
|
||
| 195 |
} |
||
| 196 |
if (vars.collSurplusPoolBefore != vars.collSurplusPoolAfter) {
|
||
| 197 |
log = log |
||
| 198 |
.concat("collSurplusPool\t\t\t")
|
||
| 199 |
.concat(vars.collSurplusPoolBefore.pretty()) |
||
| 200 |
.concat("\t")
|
||
| 201 |
.concat(vars.collSurplusPoolAfter.pretty()) |
||
| 202 |
.concat("\n");
|
||
| 203 |
} |
||
| 204 |
if (vars.nicrBefore != vars.nicrAfter) {
|
||
| 205 |
log = log |
||
| 206 |
.concat("nicr\t\t\t\t")
|
||
| 207 |
.concat(vars.nicrBefore.pretty()) |
||
| 208 |
.concat("\t")
|
||
| 209 |
.concat(vars.nicrAfter.pretty()) |
||
| 210 |
.concat("\n");
|
||
| 211 |
} |
||
| 212 |
if (vars.icrBefore != vars.icrAfter) {
|
||
| 213 |
log = log |
||
| 214 |
.concat("icr\t\t\t\t")
|
||
| 215 |
.concat(vars.icrBefore.pretty()) |
||
| 216 |
.concat("\t")
|
||
| 217 |
.concat(vars.icrAfter.pretty()) |
||
| 218 |
.concat("\n");
|
||
| 219 |
} |
||
| 220 |
if (vars.newIcrBefore != vars.newIcrAfter) {
|
||
| 221 |
log = log |
||
| 222 |
.concat("newIcr\t\t\t\t")
|
||
| 223 |
.concat(vars.newIcrBefore.pretty()) |
||
| 224 |
.concat("\t")
|
||
| 225 |
.concat(vars.newIcrAfter.pretty()) |
||
| 226 |
.concat("\n");
|
||
| 227 |
} |
||
| 228 |
if (vars.feeSplitBefore != vars.feeSplitAfter) {
|
||
| 229 |
log = log |
||
| 230 |
.concat("feeSplit\t\t\t\t")
|
||
| 231 |
.concat(vars.feeSplitBefore.pretty()) |
||
| 232 |
.concat("\t")
|
||
| 233 |
.concat(vars.feeSplitAfter.pretty()) |
||
| 234 |
.concat("\n");
|
||
| 235 |
} |
||
| 236 |
if (vars.feeRecipientTotalCollBefore != vars.feeRecipientTotalCollAfter) {
|
||
| 237 |
log = log |
||
| 238 |
.concat("feeRecipientTotalColl\t")
|
||
| 239 |
.concat(vars.feeRecipientTotalCollBefore.pretty()) |
||
| 240 |
.concat("\t")
|
||
| 241 |
.concat(vars.feeRecipientTotalCollAfter.pretty()) |
||
| 242 |
.concat("\n");
|
||
| 243 |
} |
||
| 244 |
if (vars.actorCollBefore != vars.actorCollAfter) {
|
||
| 245 |
log = log |
||
| 246 |
.concat("actorColl\t\t\t\t")
|
||
| 247 |
.concat(vars.actorCollBefore.pretty()) |
||
| 248 |
.concat("\t")
|
||
| 249 |
.concat(vars.actorCollAfter.pretty()) |
||
| 250 |
.concat("\n");
|
||
| 251 |
} |
||
| 252 |
if (vars.actorEbtcBefore != vars.actorEbtcAfter) {
|
||
| 253 |
log = log |
||
| 254 |
.concat("actorEbtc\t\t\t\t")
|
||
| 255 |
.concat(vars.actorEbtcBefore.pretty()) |
||
| 256 |
.concat("\t")
|
||
| 257 |
.concat(vars.actorEbtcAfter.pretty()) |
||
| 258 |
.concat("\n");
|
||
| 259 |
} |
||
| 260 |
if (vars.actorCdpCountBefore != vars.actorCdpCountAfter) {
|
||
| 261 |
log = log |
||
| 262 |
.concat("actorCdpCount\t\t\t")
|
||
| 263 |
.concat(vars.actorCdpCountBefore.pretty()) |
||
| 264 |
.concat("\t")
|
||
| 265 |
.concat(vars.actorCdpCountAfter.pretty()) |
||
| 266 |
.concat("\n");
|
||
| 267 |
} |
||
| 268 |
if (vars.cdpCollBefore != vars.cdpCollAfter) {
|
||
| 269 |
log = log |
||
| 270 |
.concat("cdpColl\t\t\t\t")
|
||
| 271 |
.concat(vars.cdpCollBefore.pretty()) |
||
| 272 |
.concat("\t")
|
||
| 273 |
.concat(vars.cdpCollAfter.pretty()) |
||
| 274 |
.concat("\n");
|
||
| 275 |
} |
||
| 276 |
if (vars.cdpDebtBefore != vars.cdpDebtAfter) {
|
||
| 277 |
log = log |
||
| 278 |
.concat("cdpDebt\t\t\t\t")
|
||
| 279 |
.concat(vars.cdpDebtBefore.pretty()) |
||
| 280 |
.concat("\t")
|
||
| 281 |
.concat(vars.cdpDebtAfter.pretty()) |
||
| 282 |
.concat("\n");
|
||
| 283 |
} |
||
| 284 |
if (vars.liquidatorRewardSharesBefore != vars.liquidatorRewardSharesAfter) {
|
||
| 285 |
log = log |
||
| 286 |
.concat("liquidatorRewardShares\t\t")
|
||
| 287 |
.concat(vars.liquidatorRewardSharesBefore.pretty()) |
||
| 288 |
.concat("\t")
|
||
| 289 |
.concat(vars.liquidatorRewardSharesAfter.pretty()) |
||
| 290 |
.concat("\n");
|
||
| 291 |
} |
||
| 292 |
if (vars.sortedCdpsSizeBefore != vars.sortedCdpsSizeAfter) {
|
||
| 293 |
log = log |
||
| 294 |
.concat("sortedCdpsSize\t\t\t")
|
||
| 295 |
.concat(vars.sortedCdpsSizeBefore.pretty(0)) |
||
| 296 |
.concat("\t\t\t")
|
||
| 297 |
.concat(vars.sortedCdpsSizeAfter.pretty(0)) |
||
| 298 |
.concat("\n");
|
||
| 299 |
} |
||
| 300 |
if (vars.cdpStatusBefore != vars.cdpStatusAfter) {
|
||
| 301 |
log = log |
||
| 302 |
.concat("cdpStatus\t\t\t")
|
||
| 303 |
.concat(vars.cdpStatusBefore.pretty(0)) |
||
| 304 |
.concat("\t\t\t")
|
||
| 305 |
.concat(vars.cdpStatusAfter.pretty(0)) |
||
| 306 |
.concat("\n");
|
||
| 307 |
} |
||
| 308 |
if (vars.tcrBefore != vars.tcrAfter) {
|
||
| 309 |
log = log |
||
| 310 |
.concat("tcr\t\t\t\t")
|
||
| 311 |
.concat(vars.tcrBefore.pretty()) |
||
| 312 |
.concat("\t")
|
||
| 313 |
.concat(vars.tcrAfter.pretty()) |
||
| 314 |
.concat("\n");
|
||
| 315 |
} |
||
| 316 |
if (vars.newTcrBefore != vars.newTcrAfter) {
|
||
| 317 |
log = log |
||
| 318 |
.concat("newTcr\t\t\t\t")
|
||
| 319 |
.concat(vars.newTcrBefore.pretty()) |
||
| 320 |
.concat("\t")
|
||
| 321 |
.concat(vars.newTcrAfter.pretty()) |
||
| 322 |
.concat("\n");
|
||
| 323 |
} |
||
| 324 |
if (vars.ebtcTotalSupplyBefore != vars.ebtcTotalSupplyAfter) {
|
||
| 325 |
log = log |
||
| 326 |
.concat("ebtcTotalSupply\t\t\t")
|
||
| 327 |
.concat(vars.ebtcTotalSupplyBefore.pretty()) |
||
| 328 |
.concat("\t")
|
||
| 329 |
.concat(vars.ebtcTotalSupplyAfter.pretty()) |
||
| 330 |
.concat("\n");
|
||
| 331 |
} |
||
| 332 |
if (vars.ethPerShareBefore != vars.ethPerShareAfter) {
|
||
| 333 |
log = log |
||
| 334 |
.concat("ethPerShare\t\t\t")
|
||
| 335 |
.concat(vars.ethPerShareBefore.pretty()) |
||
| 336 |
.concat("\t")
|
||
| 337 |
.concat(vars.ethPerShareAfter.pretty()) |
||
| 338 |
.concat("\n");
|
||
| 339 |
} |
||
| 340 |
if (vars.isRecoveryModeBefore != vars.isRecoveryModeAfter) {
|
||
| 341 |
log = log |
||
| 342 |
.concat("isRecoveryMode\t\t\t")
|
||
| 343 |
.concat(vars.isRecoveryModeBefore.pretty()) |
||
| 344 |
.concat("\t")
|
||
| 345 |
.concat(vars.isRecoveryModeAfter.pretty()) |
||
| 346 |
.concat("\n");
|
||
| 347 |
} |
||
| 348 |
if (vars.lastGracePeriodStartTimestampBefore != vars.lastGracePeriodStartTimestampAfter) {
|
||
| 349 |
log = log |
||
| 350 |
.concat("lastGracePeriodStartTimestamp\t")
|
||
| 351 |
.concat(vars.lastGracePeriodStartTimestampBefore.pretty()) |
||
| 352 |
.concat("\t")
|
||
| 353 |
.concat(vars.lastGracePeriodStartTimestampAfter.pretty()) |
||
| 354 |
.concat("\n");
|
||
| 355 |
} |
||
| 356 |
if ( |
||
| 357 |
vars.lastGracePeriodStartTimestampIsSetBefore != |
||
| 358 |
vars.lastGracePeriodStartTimestampIsSetAfter |
||
| 359 |
) {
|
||
| 360 |
log = log |
||
| 361 |
.concat("lastGracePeriodStartTimestampIsSet\t")
|
||
| 362 |
.concat(vars.lastGracePeriodStartTimestampIsSetBefore.pretty()) |
||
| 363 |
.concat("\t")
|
||
| 364 |
.concat(vars.lastGracePeriodStartTimestampIsSetAfter.pretty()) |
||
| 365 |
.concat("\n");
|
||
| 366 |
} |
||
| 367 |
if (vars.hasGracePeriodPassedBefore != vars.hasGracePeriodPassedAfter) {
|
||
| 368 |
log = log |
||
| 369 |
.concat("hasGracePeriodPassed\t\t")
|
||
| 370 |
.concat(vars.hasGracePeriodPassedBefore.pretty()) |
||
| 371 |
.concat("\t\t\t")
|
||
| 372 |
.concat(vars.hasGracePeriodPassedAfter.pretty()) |
||
| 373 |
.concat("\n");
|
||
| 374 |
} |
||
| 375 |
if (vars.systemDebtRedistributionIndexBefore != vars.systemDebtRedistributionIndexAfter) {
|
||
| 376 |
log = log |
||
| 377 |
.concat("systemDebtRedistributionIndex\t\t")
|
||
| 378 |
.concat(vars.systemDebtRedistributionIndexBefore.pretty()) |
||
| 379 |
.concat("\t")
|
||
| 380 |
.concat(vars.systemDebtRedistributionIndexAfter.pretty()) |
||
| 381 |
.concat("\n");
|
||
| 382 |
} |
||
| 383 |
} |
||
| 384 |
} |
||
| 385 |
| Lines covered: | 174 / 191 (91.1%) |
|---|
| 1 |
pragma solidity 0.8.17; |
||
| 2 | |||
| 3 |
import "@crytic/properties/contracts/util/PropertiesConstants.sol"; |
||
| 4 | |||
| 5 |
import {ICollateralToken} from "../../Dependencies/ICollateralToken.sol";
|
||
| 6 |
import {EbtcMath} from "../../Dependencies/EbtcMath.sol";
|
||
| 7 |
import {ActivePool} from "../../ActivePool.sol";
|
||
| 8 |
import {EBTCToken} from "../../EBTCToken.sol";
|
||
| 9 |
import {BorrowerOperations} from "../../BorrowerOperations.sol";
|
||
| 10 |
import {CdpManager} from "../../CdpManager.sol";
|
||
| 11 |
import {SortedCdps} from "../../SortedCdps.sol";
|
||
| 12 |
import {Asserts} from "./Asserts.sol";
|
||
| 13 |
import {CollSurplusPool} from "../../CollSurplusPool.sol";
|
||
| 14 |
import {PriceFeedTestnet} from "../testnet/PriceFeedTestnet.sol";
|
||
| 15 |
import {ICdpManagerData} from "../../Interfaces/ICdpManagerData.sol";
|
||
| 16 |
import {BeforeAfter} from "./BeforeAfter.sol";
|
||
| 17 |
import {PropertiesDescriptions} from "./PropertiesDescriptions.sol";
|
||
| 18 |
import {CRLens} from "../../CRLens.sol";
|
||
| 19 |
import {LiquidationSequencer} from "../../LiquidationSequencer.sol";
|
||
| 20 |
import {SyncedLiquidationSequencer} from "../../SyncedLiquidationSequencer.sol";
|
||
| 21 | |||
| 22 |
abstract contract Properties is BeforeAfter, PropertiesDescriptions, Asserts, PropertiesConstants {
|
||
| 23 |
function invariant_AP_01( |
||
| 24 |
ICollateralToken collateral, |
||
| 25 |
ActivePool activePool |
||
| 26 |
√
|
) internal view returns (bool) {
|
|
| 27 |
√
|
return (collateral.sharesOf(address(activePool)) >= activePool.getSystemCollShares()); |
|
| 28 |
} |
||
| 29 | |||
| 30 |
function invariant_AP_02( |
||
| 31 |
CdpManager cdpManager, |
||
| 32 |
ActivePool activePool |
||
| 33 |
√
|
) internal view returns (bool) {
|
|
| 34 |
√
|
return cdpManager.getActiveCdpsCount() > 0 ? activePool.getSystemCollShares() > 0 : true; |
|
| 35 |
} |
||
| 36 | |||
| 37 |
function invariant_AP_03( |
||
| 38 |
EBTCToken eBTCToken, |
||
| 39 |
ActivePool activePool |
||
| 40 |
√
|
) internal view returns (bool) {
|
|
| 41 |
√
|
return (eBTCToken.totalSupply() == activePool.getSystemDebt()); |
|
| 42 |
} |
||
| 43 | |||
| 44 |
function invariant_AP_04( |
||
| 45 |
CdpManager cdpManager, |
||
| 46 |
ActivePool activePool, |
||
| 47 |
uint256 diff_tolerance |
||
| 48 |
√
|
) internal view returns (bool) {
|
|
| 49 |
√
|
uint256 _cdpCount = cdpManager.getActiveCdpsCount(); |
|
| 50 |
√
|
uint256 _sum; |
|
| 51 |
√
|
for (uint256 i = 0; i < _cdpCount; ++i) {
|
|
| 52 |
√
|
(, uint256 _coll, ) = cdpManager.getDebtAndCollShares(cdpManager.CdpIds(i)); |
|
| 53 |
√
|
_sum += _coll; |
|
| 54 |
} |
||
| 55 |
√
|
uint256 _activeColl = activePool.getSystemCollShares(); |
|
| 56 |
√
|
uint256 _diff = _sum > _activeColl ? (_sum - _activeColl) : (_activeColl - _sum); |
|
| 57 |
√
|
return (_diff * 1e18 <= diff_tolerance * _activeColl); |
|
| 58 |
} |
||
| 59 | |||
| 60 |
function invariant_AP_05( |
||
| 61 |
CdpManager cdpManager, |
||
| 62 |
uint256 diff_tolerance |
||
| 63 |
√
|
) internal view returns (bool) {
|
|
| 64 |
√
|
uint256 _cdpCount = cdpManager.getActiveCdpsCount(); |
|
| 65 |
√
|
uint256 _sum; |
|
| 66 |
√
|
for (uint256 i = 0; i < _cdpCount; ++i) {
|
|
| 67 |
√
|
(uint256 _debt, , ) = cdpManager.getDebtAndCollShares(cdpManager.CdpIds(i)); |
|
| 68 |
√
|
_sum += _debt; |
|
| 69 |
} |
||
| 70 | |||
| 71 |
√
|
bool oldCheck = isApproximateEq(_sum, cdpManager.getSystemDebt(), diff_tolerance); |
|
| 72 |
// New check ensures this is above 1000 wei |
||
| 73 |
√
|
bool newCheck = cdpManager.getSystemDebt() - _sum > 1_000; |
|
| 74 |
// @audit We have an instance of getting above 1e18 in rounding error, see `testBrokenInvariantFive` |
||
| 75 |
√
|
return oldCheck || !newCheck; |
|
| 76 |
} |
||
| 77 | |||
| 78 |
function invariant_CDPM_01( |
||
| 79 |
CdpManager cdpManager, |
||
| 80 |
SortedCdps sortedCdps |
||
| 81 |
√
|
) internal view returns (bool) {
|
|
| 82 |
√
|
return (cdpManager.getActiveCdpsCount() == sortedCdps.getSize()); |
|
| 83 |
} |
||
| 84 | |||
| 85 |
√
|
function invariant_CDPM_02(CdpManager cdpManager) internal view returns (bool) {
|
|
| 86 |
√
|
uint256 _cdpCount = cdpManager.getActiveCdpsCount(); |
|
| 87 |
√
|
uint256 _sum; |
|
| 88 |
√
|
for (uint256 i = 0; i < _cdpCount; ++i) {
|
|
| 89 |
√
|
_sum += cdpManager.getCdpStake(cdpManager.CdpIds(i)); |
|
| 90 |
} |
||
| 91 |
√
|
return (_sum == cdpManager.totalStakes()); |
|
| 92 |
} |
||
| 93 | |||
| 94 |
√
|
function invariant_CDPM_03(CdpManager cdpManager) internal view returns (bool) {
|
|
| 95 |
√
|
uint256 _cdpCount = cdpManager.getActiveCdpsCount(); |
|
| 96 |
√
|
uint256 systemStEthFeePerUnitIndex = cdpManager.systemStEthFeePerUnitIndex(); |
|
| 97 |
√
|
for (uint256 i = 0; i < _cdpCount; ++i) {
|
|
| 98 |
if ( |
||
| 99 |
√
|
systemStEthFeePerUnitIndex < cdpManager.cdpStEthFeePerUnitIndex(cdpManager.CdpIds(i)) |
|
| 100 |
) {
|
||
| 101 |
return false; |
||
| 102 |
} |
||
| 103 |
} |
||
| 104 |
√
|
return true; |
|
| 105 |
} |
||
| 106 | |||
| 107 |
/** TODO: See EchidnaToFoundry._getValue */ |
||
| 108 |
⟳
|
function invariant_CDPM_04(Vars memory vars) internal view returns (bool) {
|
|
| 109 |
⟳
|
return vars.valueInSystemAfter >= vars.valueInSystemBefore || isApproximateEq(vars.valueInSystemAfter, vars.valueInSystemBefore, 0.01e18); |
|
| 110 |
} |
||
| 111 | |||
| 112 |
function invariant_CSP_01( |
||
| 113 |
ICollateralToken collateral, |
||
| 114 |
CollSurplusPool collSurplusPool |
||
| 115 |
√
|
) internal view returns (bool) {
|
|
| 116 |
return |
||
| 117 |
√
|
collateral.sharesOf(address(collSurplusPool)) >= |
|
| 118 |
√
|
collSurplusPool.getTotalSurplusCollShares(); |
|
| 119 |
} |
||
| 120 | |||
| 121 |
√
|
function invariant_CSP_02(CollSurplusPool collSurplusPool) internal view returns (bool) {
|
|
| 122 |
√
|
uint256 sum; |
|
| 123 | |||
| 124 |
// NOTE: See PropertiesConstants |
||
| 125 |
// We only have 3 actors so just set these up |
||
| 126 |
√
|
sum += collSurplusPool.getSurplusCollShares(address(actors[USER1])); |
|
| 127 |
√
|
sum += collSurplusPool.getSurplusCollShares(address(actors[USER2])); |
|
| 128 |
√
|
sum += collSurplusPool.getSurplusCollShares(address(actors[USER3])); |
|
| 129 | |||
| 130 |
√
|
return sum == collSurplusPool.getTotalSurplusCollShares(); |
|
| 131 |
} |
||
| 132 | |||
| 133 |
√
|
function invariant_SL_01(CdpManager cdpManager, SortedCdps sortedCdps) internal returns (bool) {
|
|
| 134 |
√
|
bytes32 currentCdp = sortedCdps.getFirst(); |
|
| 135 |
√
|
bytes32 nextCdp = sortedCdps.getNext(currentCdp); |
|
| 136 | |||
| 137 |
√
|
while (currentCdp != bytes32(0) && nextCdp != bytes32(0) && currentCdp != nextCdp) {
|
|
| 138 |
// TODO remove tolerance once proper fix has been applied |
||
| 139 |
√
|
uint256 nicrNext = cdpManager.getNominalICR(nextCdp); |
|
| 140 |
√
|
uint256 nicrCurrent = cdpManager.getNominalICR(currentCdp); |
|
| 141 |
√
|
emit L2(nicrNext, nicrCurrent); |
|
| 142 |
√
|
if (nicrNext > nicrCurrent && diffPercent(nicrNext, nicrCurrent) > 0.01e18) {
|
|
| 143 |
return false; |
||
| 144 |
} |
||
| 145 | |||
| 146 |
√
|
currentCdp = nextCdp; |
|
| 147 |
√
|
nextCdp = sortedCdps.getNext(currentCdp); |
|
| 148 |
} |
||
| 149 | |||
| 150 |
√
|
return true; |
|
| 151 |
} |
||
| 152 | |||
| 153 |
function invariant_SL_02( |
||
| 154 |
CdpManager cdpManager, |
||
| 155 |
SortedCdps sortedCdps, |
||
| 156 |
PriceFeedTestnet priceFeedMock |
||
| 157 |
√
|
) internal view returns (bool) {
|
|
| 158 |
√
|
bytes32 _first = sortedCdps.getFirst(); |
|
| 159 |
√
|
uint256 _price = priceFeedMock.getPrice(); |
|
| 160 |
√
|
uint256 _firstICR = cdpManager.getICR(_first, _price); |
|
| 161 |
√
|
uint256 _TCR = cdpManager.getTCR(_price); |
|
| 162 | |||
| 163 |
if ( |
||
| 164 |
√
|
_first != sortedCdps.dummyId() && |
|
| 165 |
√
|
_firstICR < _TCR && |
|
| 166 |
diffPercent(_firstICR, _TCR) > 0.01e18 |
||
| 167 |
) {
|
||
| 168 |
return false; |
||
| 169 |
} |
||
| 170 |
√
|
return true; |
|
| 171 |
} |
||
| 172 | |||
| 173 |
function invariant_SL_03( |
||
| 174 |
CdpManager cdpManager, |
||
| 175 |
PriceFeedTestnet priceFeedMock, |
||
| 176 |
SortedCdps sortedCdps |
||
| 177 |
√
|
) internal view returns (bool) {
|
|
| 178 |
√
|
bytes32 currentCdp = sortedCdps.getFirst(); |
|
| 179 | |||
| 180 |
√
|
uint256 _price = priceFeedMock.getPrice(); |
|
| 181 |
√
|
if (_price == 0) return true; |
|
| 182 | |||
| 183 |
√
|
while (currentCdp != bytes32(0)) {
|
|
| 184 |
// Status |
||
| 185 |
if ( |
||
| 186 |
√
|
ICdpManagerData.Status(cdpManager.getCdpStatus(currentCdp)) != |
|
| 187 |
√
|
ICdpManagerData.Status.active |
|
| 188 |
) {
|
||
| 189 |
return false; |
||
| 190 |
} |
||
| 191 | |||
| 192 |
// Stake > 0 |
||
| 193 |
√
|
if (cdpManager.getCdpStake(currentCdp) == 0) {
|
|
| 194 |
return false; |
||
| 195 |
} |
||
| 196 | |||
| 197 |
√
|
currentCdp = sortedCdps.getNext(currentCdp); |
|
| 198 |
} |
||
| 199 |
√
|
return true; |
|
| 200 |
} |
||
| 201 | |||
| 202 |
√
|
uint256 NICR_ERROR_THRESHOLD = 1e8; |
|
| 203 | |||
| 204 |
√
|
function invariant_SL_05(CRLens crLens, SortedCdps sortedCdps) internal returns (bool) {
|
|
| 205 |
√
|
bytes32 currentCdp = sortedCdps.getFirst(); |
|
| 206 | |||
| 207 |
√
|
uint256 newIcrPrevious = type(uint256).max; |
|
| 208 | |||
| 209 |
√
|
while (currentCdp != bytes32(0)) {
|
|
| 210 |
√
|
uint256 newIcr = crLens.quoteRealICR(currentCdp); |
|
| 211 |
√
|
if (newIcr > newIcrPrevious) {
|
|
| 212 |
/// @audit Precision Threshold to flag very scary scenarios |
||
| 213 |
/// Innoquous scenario illustrated here: https://github.com/Badger-Finance/ebtc-fuzz-review/issues/15 |
||
| 214 |
if (newIcr - newIcrPrevious > NICR_ERROR_THRESHOLD) {
|
||
| 215 |
return false; |
||
| 216 |
} |
||
| 217 |
} |
||
| 218 |
√
|
newIcrPrevious = newIcr; |
|
| 219 | |||
| 220 |
√
|
currentCdp = sortedCdps.getNext(currentCdp); |
|
| 221 |
} |
||
| 222 |
√
|
return true; |
|
| 223 |
} |
||
| 224 | |||
| 225 |
√
|
function invariant_GENERAL_01(Vars memory vars) internal view returns (bool) {
|
|
| 226 |
√
|
return !vars.isRecoveryModeBefore ? !vars.isRecoveryModeAfter : true; |
|
| 227 |
} |
||
| 228 | |||
| 229 |
function invariant_GENERAL_02( |
||
| 230 |
CdpManager cdpManager, |
||
| 231 |
PriceFeedTestnet priceFeedMock, |
||
| 232 |
EBTCToken eBTCToken |
||
| 233 |
√
|
) internal view returns (bool) {
|
|
| 234 |
// TODO how to calculate "the dollar value of eBTC"? |
||
| 235 |
// TODO how do we take into account underlying/shares into this calculation? |
||
| 236 |
return |
||
| 237 |
√
|
cdpManager.getTCR(priceFeedMock.getPrice()) > collateral.getPooledEthByShares(1e18) |
|
| 238 |
√
|
? (cdpManager.getSystemCollShares() * priceFeedMock.getPrice()) / 1e18 >= |
|
| 239 |
√
|
eBTCToken.totalSupply() |
|
| 240 |
: (cdpManager.getSystemCollShares() * priceFeedMock.getPrice()) / 1e18 < |
||
| 241 |
eBTCToken.totalSupply(); |
||
| 242 |
} |
||
| 243 | |||
| 244 |
function invariant_GENERAL_03( |
||
| 245 |
CdpManager cdpManager, |
||
| 246 |
BorrowerOperations borrowerOperations, |
||
| 247 |
EBTCToken eBTCToken, |
||
| 248 |
ICollateralToken collateral |
||
| 249 |
√
|
) internal view returns (bool) {
|
|
| 250 |
return |
||
| 251 |
√
|
collateral.balanceOf(address(cdpManager)) == 0 && |
|
| 252 |
√
|
eBTCToken.balanceOf(address(cdpManager)) == 0 && |
|
| 253 |
√
|
collateral.balanceOf(address(borrowerOperations)) == 0 && |
|
| 254 |
√
|
eBTCToken.balanceOf(address(borrowerOperations)) == 0; |
|
| 255 |
} |
||
| 256 | |||
| 257 |
function invariant_GENERAL_05( |
||
| 258 |
ActivePool activePool, |
||
| 259 |
CdpManager cdpManager, |
||
| 260 |
ICollateralToken collateral |
||
| 261 |
√
|
) internal view returns (bool) {
|
|
| 262 |
√
|
uint256 totalStipendShares; |
|
| 263 | |||
| 264 |
// Iterate over CDPs add the stipendShares |
||
| 265 |
√
|
bytes32 currentCdp = sortedCdps.getFirst(); |
|
| 266 |
√
|
while (currentCdp != bytes32(0)) {
|
|
| 267 |
√
|
totalStipendShares += cdpManager.getCdpLiquidatorRewardShares(currentCdp); |
|
| 268 | |||
| 269 |
√
|
currentCdp = sortedCdps.getNext(currentCdp); |
|
| 270 |
} |
||
| 271 | |||
| 272 |
return |
||
| 273 |
√
|
collateral.sharesOf(address(activePool)) >= |
|
| 274 |
√
|
(activePool.getSystemCollShares() + |
|
| 275 |
√
|
activePool.getFeeRecipientClaimableCollShares() + |
|
| 276 |
√
|
totalStipendShares); |
|
| 277 |
} |
||
| 278 | |||
| 279 |
function invariant_GENERAL_05_B( |
||
| 280 |
CollSurplusPool surplusPool, |
||
| 281 |
ICollateralToken collateral |
||
| 282 |
√
|
) internal view returns (bool) {
|
|
| 283 |
return |
||
| 284 |
√
|
collateral.sharesOf(address(surplusPool)) >= (surplusPool.getTotalSurplusCollShares()); |
|
| 285 |
} |
||
| 286 | |||
| 287 |
function invariant_GENERAL_06( |
||
| 288 |
EBTCToken eBTCToken, |
||
| 289 |
CdpManager cdpManager, |
||
| 290 |
SortedCdps sortedCdps |
||
| 291 |
√
|
) internal view returns (bool) {
|
|
| 292 |
√
|
uint256 totalSupply = eBTCToken.totalSupply(); |
|
| 293 | |||
| 294 |
√
|
bytes32 currentCdp = sortedCdps.getFirst(); |
|
| 295 |
√
|
uint256 cdpsBalance; |
|
| 296 |
√
|
while (currentCdp != bytes32(0)) {
|
|
| 297 |
√
|
(uint256 entireDebt, , ) = cdpManager.getDebtAndCollShares(currentCdp); |
|
| 298 |
√
|
cdpsBalance += entireDebt; |
|
| 299 |
√
|
currentCdp = sortedCdps.getNext(currentCdp); |
|
| 300 |
} |
||
| 301 | |||
| 302 |
√
|
return totalSupply >= cdpsBalance; |
|
| 303 |
} |
||
| 304 | |||
| 305 |
function invariant_GENERAL_08( |
||
| 306 |
CdpManager cdpManager, |
||
| 307 |
SortedCdps sortedCdps, |
||
| 308 |
PriceFeedTestnet priceFeedTestnet, |
||
| 309 |
ICollateralToken collateral |
||
| 310 |
√
|
) internal view returns (bool) {
|
|
| 311 |
√
|
uint256 curentPrice = priceFeedTestnet.getPrice(); |
|
| 312 | |||
| 313 |
√
|
bytes32 currentCdp = sortedCdps.getFirst(); |
|
| 314 | |||
| 315 |
√
|
uint256 sumOfColl; |
|
| 316 |
√
|
uint256 sumOfDebt; |
|
| 317 |
√
|
while (currentCdp != bytes32(0)) {
|
|
| 318 |
√
|
uint256 entireColl = cdpManager.getSyncedCdpCollShares(currentCdp); |
|
| 319 |
√
|
uint256 entireDebt = cdpManager.getSyncedCdpDebt(currentCdp); |
|
| 320 |
√
|
sumOfColl += entireColl; |
|
| 321 |
√
|
sumOfDebt += entireDebt; |
|
| 322 |
√
|
currentCdp = sortedCdps.getNext(currentCdp); |
|
| 323 |
} |
||
| 324 | |||
| 325 |
√
|
uint256 tcrFromSystem = cdpManager.getSyncedTCR(curentPrice); |
|
| 326 | |||
| 327 |
√
|
uint256 tcrFromSums = EbtcMath._computeCR( |
|
| 328 |
√
|
collateral.getPooledEthByShares(sumOfColl), |
|
| 329 |
√
|
sumOfDebt, |
|
| 330 |
√
|
curentPrice |
|
| 331 |
); |
||
| 332 |
/// @audit 1e8 precision |
||
| 333 |
√
|
return isApproximateEq(tcrFromSystem, tcrFromSums, 1e8); // Up to 1e8 precision is accepted |
|
| 334 |
} |
||
| 335 | |||
| 336 |
function invariant_GENERAL_09( |
||
| 337 |
CdpManager cdpManager, |
||
| 338 |
Vars memory vars |
||
| 339 |
√
|
) internal view returns (bool) {
|
|
| 340 |
√
|
if (vars.isRecoveryModeBefore) {
|
|
| 341 |
if (vars.cdpDebtAfter > vars.cdpDebtBefore) return (vars.icrAfter > cdpManager.MCR()); |
||
| 342 |
else return true; |
||
| 343 |
} else {
|
||
| 344 |
√
|
return (vars.icrAfter > cdpManager.MCR()); |
|
| 345 |
} |
||
| 346 |
} |
||
| 347 | |||
| 348 |
function invariant_GENERAL_12( |
||
| 349 |
CdpManager cdpManager, |
||
| 350 |
PriceFeedTestnet priceFeedMock, |
||
| 351 |
CRLens crLens |
||
| 352 |
√
|
) internal returns (bool) {
|
|
| 353 |
√
|
uint256 curentPrice = priceFeedMock.getPrice(); |
|
| 354 |
√
|
return crLens.quoteRealTCR() == cdpManager.getSyncedTCR(curentPrice); |
|
| 355 |
} |
||
| 356 | |||
| 357 |
function invariant_GENERAL_13( |
||
| 358 |
CRLens crLens, |
||
| 359 |
CdpManager cdpManager, |
||
| 360 |
PriceFeedTestnet priceFeedMock, |
||
| 361 |
SortedCdps sortedCdps |
||
| 362 |
√
|
) internal returns (bool) {
|
|
| 363 |
√
|
bytes32 currentCdp = sortedCdps.getFirst(); |
|
| 364 | |||
| 365 |
√
|
uint256 _price = priceFeedMock.getPrice(); |
|
| 366 | |||
| 367 |
// Compare synched with quote for all Cdps |
||
| 368 |
√
|
while (currentCdp != bytes32(0)) {
|
|
| 369 |
√
|
uint256 newIcr = crLens.quoteRealICR(currentCdp); |
|
| 370 |
√
|
uint256 synchedICR = cdpManager.getSyncedICR(currentCdp, _price); |
|
| 371 | |||
| 372 |
√
|
if (newIcr != synchedICR) {
|
|
| 373 |
return false; |
||
| 374 |
} |
||
| 375 | |||
| 376 |
√
|
currentCdp = sortedCdps.getNext(currentCdp); |
|
| 377 |
} |
||
| 378 |
√
|
return true; |
|
| 379 |
} |
||
| 380 | |||
| 381 |
function invariant_GENERAL_14( |
||
| 382 |
CRLens crLens, |
||
| 383 |
CdpManager cdpManager, |
||
| 384 |
SortedCdps sortedCdps |
||
| 385 |
√
|
) internal returns (bool) {
|
|
| 386 |
√
|
bytes32 currentCdp = sortedCdps.getFirst(); |
|
| 387 | |||
| 388 |
√
|
uint256 newIcrPrevious = type(uint256).max; |
|
| 389 | |||
| 390 |
// Compare synched with quote for all Cdps |
||
| 391 |
√
|
while (currentCdp != bytes32(0)) {
|
|
| 392 |
√
|
uint256 newNICR = crLens.quoteRealNICR(currentCdp); |
|
| 393 |
√
|
uint256 synchedNICR = cdpManager.getSyncedNominalICR(currentCdp); // Uses cached stETH index -> It's not the "real NICR" |
|
| 394 | |||
| 395 |
√
|
if (newNICR != synchedNICR) {
|
|
| 396 |
return false; |
||
| 397 |
} |
||
| 398 | |||
| 399 |
√
|
currentCdp = sortedCdps.getNext(currentCdp); |
|
| 400 |
} |
||
| 401 |
√
|
return true; |
|
| 402 |
} |
||
| 403 | |||
| 404 |
√
|
function invariant_GENERAL_15() internal returns (bool) {
|
|
| 405 |
√
|
return crLens.quoteAnything(simulator.simulateRepayEverythingAndCloseCdps) == simulator.TRUE(); |
|
| 406 |
} |
||
| 407 | |||
| 408 |
function invariant_LS_01( |
||
| 409 |
CdpManager cdpManager, |
||
| 410 |
LiquidationSequencer ls, |
||
| 411 |
SyncedLiquidationSequencer syncedLs, |
||
| 412 |
PriceFeedTestnet priceFeedTestnet |
||
| 413 |
√
|
) internal returns (bool) {
|
|
| 414 |
// Or just compare max lenght since that's the one with all of them |
||
| 415 |
√
|
uint256 n = cdpManager.getActiveCdpsCount(); |
|
| 416 | |||
| 417 |
// Get |
||
| 418 |
√
|
uint256 price = priceFeedTestnet.getPrice(); |
|
| 419 | |||
| 420 |
// Get lists |
||
| 421 |
√
|
bytes32[] memory cdpsFromCurrent = ls.sequenceLiqToBatchLiqWithPrice(n, price); |
|
| 422 |
√
|
bytes32[] memory cdpsSynced = syncedLs.sequenceLiqToBatchLiqWithPrice(n, price); |
|
| 423 | |||
| 424 |
√
|
uint256 length = cdpsFromCurrent.length; |
|
| 425 |
√
|
if (length != cdpsSynced.length) {
|
|
| 426 |
return false; |
||
| 427 |
} |
||
| 428 | |||
| 429 |
// Compare Lists |
||
| 430 |
√
|
for (uint256 i; i < length; i++) {
|
|
| 431 |
// Find difference = broken |
||
| 432 |
if (cdpsFromCurrent[i] != cdpsSynced[i]) {
|
||
| 433 |
return false; |
||
| 434 |
} |
||
| 435 |
} |
||
| 436 | |||
| 437 |
// Implies we're good |
||
| 438 |
√
|
return true; |
|
| 439 |
} |
||
| 440 | |||
| 441 |
√
|
function invariant_DUMMY_01(PriceFeedTestnet priceFeedTestnet) internal view returns (bool) {
|
|
| 442 |
√
|
return priceFeedTestnet.getPrice() > 0; |
|
| 443 |
} |
||
| 444 |
} |
||
| 445 |
| Lines covered: | 0 / 0 (0.0%) |
|---|
| 1 |
pragma solidity 0.8.17; |
||
| 2 | |||
| 3 |
abstract contract PropertiesDescriptions {
|
||
| 4 |
/////////////////////////////////////////////////////// |
||
| 5 |
// Active Pool |
||
| 6 |
/////////////////////////////////////////////////////// |
||
| 7 | |||
| 8 |
string constant AP_01 = |
||
| 9 |
"AP-01: The collateral balance in the active pool is greater than or equal to its accounting number"; |
||
| 10 |
string constant AP_02 = |
||
| 11 |
"AP-06: The collateral balance of the ActivePool is positive if there is at least one CDP open"; |
||
| 12 |
string constant AP_03 = |
||
| 13 |
"AP-03: The eBTC debt accounting number in active pool is greater than or equal to its accounting number"; |
||
| 14 |
string constant AP_04 = |
||
| 15 |
"AP-04: The total collateral in active pool should be equal to the sum of all individual CDP collateral"; |
||
| 16 |
string constant AP_05 = |
||
| 17 |
"AP-05: The sum of debt accounting in active pool should be equal to sum of debt accounting of individual CDPs"; |
||
| 18 | |||
| 19 |
/////////////////////////////////////////////////////// |
||
| 20 |
// Cdp Manager |
||
| 21 |
/////////////////////////////////////////////////////// |
||
| 22 | |||
| 23 |
string constant CDPM_01 = |
||
| 24 |
"CDPM-01: The count of active CDPs is equal to the SortedCdp list length"; |
||
| 25 |
string constant CDPM_02 = "CDPM-02: The sum of active CDPs stake is equal to totalStakes"; |
||
| 26 |
string constant CDPM_03 = |
||
| 27 |
"CDPM-03: The stFeePerUnit tracker for individual CDP is equal to or less than the global variable"; |
||
| 28 |
string constant CDPM_04 = "CDPM-04: The total system value does not decrease during redemptions"; |
||
| 29 |
string constant CDPM_05 = "CDPM-05: Redemptions do not increase the total system debt"; |
||
| 30 |
string constant CDPM_06 = "CDPM-06: Redemptions do not increase the total system debt"; |
||
| 31 | |||
| 32 |
/////////////////////////////////////////////////////// |
||
| 33 |
// Collateral Surplus Pool |
||
| 34 |
/////////////////////////////////////////////////////// |
||
| 35 | |||
| 36 |
string constant CSP_01 = |
||
| 37 |
"CSP-01: The collateral balance in the collSurplus pool is greater than or equal to its accounting number"; |
||
| 38 | |||
| 39 |
/////////////////////////////////////////////////////// |
||
| 40 |
// Sorted List |
||
| 41 |
/////////////////////////////////////////////////////// |
||
| 42 | |||
| 43 |
string constant SL_01 = |
||
| 44 |
"SL-01: The NICR ranking in the sorted list should follow descending order"; |
||
| 45 |
string constant SL_02 = |
||
| 46 |
"SL-02: The the first(highest) ICR in the sorted list should be greater or equal to TCR (with tolerance due to rounding errors)"; |
||
| 47 |
string constant SL_03 = "SL-03: All CDPs have status active and stake greater than zero"; |
||
| 48 |
string constant SL_05 = |
||
| 49 |
"SL-05: The CDPs should be sorted in descending order of new ICR (accrued)"; |
||
| 50 | |||
| 51 |
/////////////////////////////////////////////////////// |
||
| 52 |
// Borrower Operations |
||
| 53 |
/////////////////////////////////////////////////////// |
||
| 54 | |||
| 55 |
string constant BO_01 = "BO-01: Users can only open CDPs with healthy ICR"; |
||
| 56 |
string constant BO_02 = "BO-02: Users must repay all debt to close a CDP"; |
||
| 57 |
string constant BO_03 = "BO-03: Adding collateral doesn't reduce Nominal ICR"; |
||
| 58 |
string constant BO_04 = "BO-04: Removing collateral does not increase the Nominal ICR"; |
||
| 59 |
string constant BO_05 = |
||
| 60 |
"BO-05: When a borrower closes their active CDP, the gas compensation is refunded to the user"; |
||
| 61 |
string constant BO_07 = "BO-07: eBTC tokens are burned upon repayment of a CDP's debt"; |
||
| 62 |
string constant BO_08 = "BO-08: TCR must increase after a repayment"; |
||
| 63 | |||
| 64 |
/////////////////////////////////////////////////////// |
||
| 65 |
// General |
||
| 66 |
/////////////////////////////////////////////////////// |
||
| 67 |
string constant GENERAL_01 = |
||
| 68 |
"GENERAL-01: After any operation, the system should not enter in Recovery Mode"; |
||
| 69 |
string constant GENERAL_02 = |
||
| 70 |
"GENERAL-02: The dollar value of the locked stETH exceeds the dollar value of the issued eBTC if TCR is greater than 100%"; |
||
| 71 |
string constant GENERAL_03 = |
||
| 72 |
"GENERAL-03: CdpManager and BorrowerOperations do not hold value terms of stETH and eBTC unless there are donations"; |
||
| 73 |
string constant GENERAL_05 = |
||
| 74 |
"GENERAL-05: At all times, the total stETH shares of the system exceeds the deposits if there is no negative rebasing events"; |
||
| 75 |
string constant GENERAL_06 = |
||
| 76 |
"GENERAL-06: At all times, the total debt is greater than the sum of all debts from all CDPs"; |
||
| 77 |
string constant GENERAL_08 = |
||
| 78 |
"GENERAL-08: At all times TCR = SUM(COLL) * price / SUM(DEBT) of all CDPs"; |
||
| 79 |
string constant GENERAL_09 = |
||
| 80 |
"GENERAL-09: After any operation, the ICR of a CDP must be above the MCR in Normal Mode, and after debt increase in Recovery Mode the ICR must be above the CCR"; |
||
| 81 |
string constant GENERAL_10 = "GENERAL-10: All CDPs should maintain a minimum collateral size"; |
||
| 82 |
string constant GENERAL_11 = |
||
| 83 |
"GENERAL-11: The TCR pre-computed (TCRNotified) is the same as the one after all calls"; |
||
| 84 |
string constant GENERAL_12 = |
||
| 85 |
"GENERAL-12: The synchedTCR matches the TCR after accrual (as returned by CrLens)"; |
||
| 86 |
string constant GENERAL_13 = |
||
| 87 |
"GENERAL-13: The SynchedICR of every CDP in the Linked List Matches the ICR the CDPs will have the call (as returned by CrLens)"; |
||
| 88 |
string constant GENERAL_14 = |
||
| 89 |
"GENERAL-14: The NominalICR from `getNominalICR` matches `quoteRealNICR` (as returned by CrLens)"; |
||
| 90 | |||
| 91 |
/////////////////////////////////////////////////////// |
||
| 92 |
// Redemptions |
||
| 93 |
/////////////////////////////////////////////////////// |
||
| 94 | |||
| 95 |
string constant R_07 = "R-07: TCR should not decrease after redemptions"; |
||
| 96 |
string constant R_08 = "R-08: The user eBTC balance should be used to pay the system debt"; |
||
| 97 | |||
| 98 |
/////////////////////////////////////////////////////// |
||
| 99 |
// Liquidations |
||
| 100 |
/////////////////////////////////////////////////////// |
||
| 101 | |||
| 102 |
string constant L_01 = |
||
| 103 |
"L-01: Liquidation only succeeds if ICR < 110% in Normal Mode, or if ICR < 125% in Recovery Mode"; |
||
| 104 |
string constant L_09 = |
||
| 105 |
"L-09: Undercollateralized liquidations are also incentivized with the Gas Stipend"; |
||
| 106 |
string constant L_12 = "L-12: TCR must increase after liquidation with no redistributions"; |
||
| 107 |
string constant L_14 = |
||
| 108 |
"If the RM grace period is set and we're in recovery mode, new actions that keep the system in recovery mode should not change the cooldown timestamp"; |
||
| 109 |
string constant L_15 = |
||
| 110 |
"L-15: The RM grace period should set if a BO/liquidation/redistribution makes the TCR above CCR"; |
||
| 111 |
string constant L_16 = |
||
| 112 |
"L-16: The RM grace period should reset if a BO/liquidation/redistribution makes the TCR below CCR"; |
||
| 113 | |||
| 114 |
/////////////////////////////////////////////////////// |
||
| 115 |
// eBTC |
||
| 116 |
/////////////////////////////////////////////////////// |
||
| 117 | |||
| 118 |
string constant EBTC_02 = |
||
| 119 |
"EBTC-02: Any eBTC holder (whether or not they have an active CDP) may redeem their eBTC unless TCR is below MCR"; |
||
| 120 | |||
| 121 |
/////////////////////////////////////////////////////// |
||
| 122 |
// Fee Recipient |
||
| 123 |
/////////////////////////////////////////////////////// |
||
| 124 | |||
| 125 |
string constant F_01 = "F-01: `claimFeeRecipientCollShares` allows to claim at any time"; |
||
| 126 |
string constant F_02 = "F-02: Fees From Redemptions are added to `claimFeeRecipientCollShares`"; |
||
| 127 |
string constant F_03 = "F-03: Fees From FlashLoans are sent to the fee Recipient"; |
||
| 128 |
/////////////////////////////////////////////////////// |
||
| 129 |
// Price Feed |
||
| 130 |
/////////////////////////////////////////////////////// |
||
| 131 | |||
| 132 |
string constant PF_01 = "PF-01: The price feed must never revert"; |
||
| 133 |
string constant PF_02 = "PF-02: The price feed must follow valid status transitions"; |
||
| 134 |
string constant PF_03 = "PF-03: The price feed must never deadlock"; |
||
| 135 |
string constant PF_04 = |
||
| 136 |
"PF-04: The price feed should never report an outdated price if chainlink is Working"; |
||
| 137 |
string constant PF_05 = |
||
| 138 |
"PF-05: The price feed should never use the fallback if chainlink is Working"; |
||
| 139 |
string constant PF_06 = "PF-06: The system never tries to use the fallback if it is not set"; |
||
| 140 |
} |
||
| 141 |
| Lines covered: | 20 / 20 (100.0%) |
|---|
| 1 |
pragma solidity 0.8.17; |
||
| 2 | |||
| 3 |
import {BorrowerOperations} from "../../BorrowerOperations.sol";
|
||
| 4 |
import {CdpManager} from "../../CdpManager.sol";
|
||
| 5 |
import {SortedCdps} from "../../SortedCdps.sol";
|
||
| 6 |
import {Actor} from "./Actor.sol";
|
||
| 7 | |||
| 8 |
contract Simulator {
|
||
| 9 |
√
|
⟳
|
uint256 public constant TRUE = uint256(keccak256(abi.encodePacked("TRUE")));
|
| 10 |
event Log(string); |
||
| 11 | |||
| 12 |
Actor[] private actors; |
||
| 13 |
CdpManager private cdpManager; |
||
| 14 |
SortedCdps private sortedCdps; |
||
| 15 |
BorrowerOperations private borrowerOperations; |
||
| 16 | |||
| 17 |
constructor( |
||
| 18 |
Actor[] memory _actors, |
||
| 19 |
CdpManager _cdpManager, |
||
| 20 |
SortedCdps _sortedCdps, |
||
| 21 |
BorrowerOperations _borrowerOperations |
||
| 22 |
) {
|
||
| 23 |
√
|
actors = _actors; |
|
| 24 |
√
|
cdpManager = _cdpManager; |
|
| 25 |
√
|
sortedCdps = _sortedCdps; |
|
| 26 |
√
|
borrowerOperations = _borrowerOperations; |
|
| 27 |
} |
||
| 28 | |||
| 29 |
function simulateRepayEverythingAndCloseCdps() external {
|
||
| 30 |
⟳
|
bool success; |
|
| 31 | |||
| 32 |
⟳
|
bytes32 currentCdp = sortedCdps.getFirst(); |
|
| 33 |
⟳
|
while (currentCdp != bytes32(0) && sortedCdps.getSize() > 1) {
|
|
| 34 |
⟳
|
Actor actor = Actor(payable(sortedCdps.getOwnerAddress(currentCdp))); |
|
| 35 |
⟳
|
(uint256 entireDebt, , ) = cdpManager.getDebtAndCollShares(currentCdp); |
|
| 36 | |||
| 37 |
⟳
|
(success, ) = actor.proxy( |
|
| 38 |
⟳
|
address(borrowerOperations), |
|
| 39 |
⟳
|
abi.encodeWithSelector(BorrowerOperations.closeCdp.selector, currentCdp) |
|
| 40 |
); |
||
| 41 |
⟳
|
require(success); |
|
| 42 | |||
| 43 |
⟳
|
currentCdp = sortedCdps.getNext(currentCdp); |
|
| 44 |
} |
||
| 45 | |||
| 46 |
⟳
|
_success(); |
|
| 47 |
} |
||
| 48 | |||
| 49 |
function _success() private {
|
||
| 50 |
⟳
|
uint256 ans = TRUE; |
|
| 51 |
assembly {
|
||
| 52 |
⟳
|
let ptr := mload(0x40) |
|
| 53 |
⟳
|
mstore(ptr, ans) |
|
| 54 |
⟳
|
revert(ptr, 32) |
|
| 55 |
} |
||
| 56 |
} |
||
| 57 |
} |
||
| 58 |
| Lines covered: | 202 / 202 (100.0%) |
|---|
| 1 |
pragma solidity 0.8.17; |
||
| 2 | |||
| 3 |
import "@crytic/properties/contracts/util/PropertiesConstants.sol"; |
||
| 4 | |||
| 5 |
import "../../Interfaces/ICdpManagerData.sol"; |
||
| 6 |
import "../../Dependencies/SafeMath.sol"; |
||
| 7 |
import "../../CdpManager.sol"; |
||
| 8 |
import "../../LiquidationLibrary.sol"; |
||
| 9 |
import "../../BorrowerOperations.sol"; |
||
| 10 |
import "../../ActivePool.sol"; |
||
| 11 |
import "../../CollSurplusPool.sol"; |
||
| 12 |
import "../../SortedCdps.sol"; |
||
| 13 |
import "../../HintHelpers.sol"; |
||
| 14 |
import "../../FeeRecipient.sol"; |
||
| 15 |
import "../testnet/PriceFeedTestnet.sol"; |
||
| 16 |
import "../CollateralTokenTester.sol"; |
||
| 17 |
import "../EBTCTokenTester.sol"; |
||
| 18 |
import "../../Governor.sol"; |
||
| 19 |
import "../../EBTCDeployer.sol"; |
||
| 20 | |||
| 21 |
import "./Properties.sol"; |
||
| 22 |
import "./Actor.sol"; |
||
| 23 |
import "../BaseStorageVariables.sol"; |
||
| 24 | |||
| 25 |
abstract contract TargetContractSetup is BaseStorageVariables, PropertiesConstants {
|
||
| 26 |
using SafeMath for uint; |
||
| 27 | |||
| 28 |
√
|
bytes4 internal constant BURN_SIG = bytes4(keccak256(bytes("burn(address,uint256)")));
|
|
| 29 | |||
| 30 |
uint internal numberOfCdps; |
||
| 31 | |||
| 32 |
struct CDPChange {
|
||
| 33 |
uint collAddition; |
||
| 34 |
uint collReduction; |
||
| 35 |
uint debtAddition; |
||
| 36 |
uint debtReduction; |
||
| 37 |
} |
||
| 38 | |||
| 39 |
function _setUp() internal {
|
||
| 40 |
√
|
defaultGovernance = address(this); |
|
| 41 |
√
|
ebtcDeployer = new EBTCDeployer(); |
|
| 42 | |||
| 43 |
// Default governance is deployer |
||
| 44 |
// vm.prank(defaultGovernance); |
||
| 45 |
√
|
collateral = new CollateralTokenTester(); |
|
| 46 | |||
| 47 |
√
|
EBTCDeployer.EbtcAddresses memory addr = ebtcDeployer.getFutureEbtcAddresses(); |
|
| 48 | |||
| 49 |
{
|
||
| 50 |
√
|
bytes memory creationCode; |
|
| 51 |
√
|
bytes memory args; |
|
| 52 | |||
| 53 |
// Use EBTCDeployer to deploy all contracts at determistic addresses |
||
| 54 | |||
| 55 |
// Authority |
||
| 56 |
√
|
creationCode = type(Governor).creationCode; |
|
| 57 |
√
|
args = abi.encode(address(this)); |
|
| 58 | |||
| 59 |
√
|
authority = Governor( |
|
| 60 |
√
|
ebtcDeployer.deploy(ebtcDeployer.AUTHORITY(), abi.encodePacked(creationCode, args)) |
|
| 61 |
); |
||
| 62 | |||
| 63 |
// Liquidation Library |
||
| 64 |
√
|
creationCode = type(LiquidationLibrary).creationCode; |
|
| 65 |
args = abi.encode( |
||
| 66 |
√
|
addr.borrowerOperationsAddress, |
|
| 67 |
√
|
addr.collSurplusPoolAddress, |
|
| 68 |
√
|
addr.ebtcTokenAddress, |
|
| 69 |
√
|
addr.sortedCdpsAddress, |
|
| 70 |
√
|
addr.activePoolAddress, |
|
| 71 |
√
|
addr.priceFeedAddress, |
|
| 72 |
√
|
address(collateral) |
|
| 73 |
); |
||
| 74 | |||
| 75 |
√
|
liqudationLibrary = LiquidationLibrary( |
|
| 76 |
√
|
ebtcDeployer.deploy( |
|
| 77 |
√
|
ebtcDeployer.LIQUIDATION_LIBRARY(), |
|
| 78 |
√
|
abi.encodePacked(creationCode, args) |
|
| 79 |
) |
||
| 80 |
); |
||
| 81 | |||
| 82 |
// CDP Manager |
||
| 83 |
√
|
creationCode = type(CdpManager).creationCode; |
|
| 84 |
args = abi.encode( |
||
| 85 |
√
|
addr.liquidationLibraryAddress, |
|
| 86 |
√
|
addr.authorityAddress, |
|
| 87 |
√
|
addr.borrowerOperationsAddress, |
|
| 88 |
√
|
addr.collSurplusPoolAddress, |
|
| 89 |
√
|
addr.ebtcTokenAddress, |
|
| 90 |
√
|
addr.sortedCdpsAddress, |
|
| 91 |
√
|
addr.activePoolAddress, |
|
| 92 |
√
|
addr.priceFeedAddress, |
|
| 93 |
√
|
address(collateral) |
|
| 94 |
); |
||
| 95 | |||
| 96 |
√
|
cdpManager = CdpManager( |
|
| 97 |
√
|
ebtcDeployer.deploy(ebtcDeployer.CDP_MANAGER(), abi.encodePacked(creationCode, args)) |
|
| 98 |
); |
||
| 99 | |||
| 100 |
// Borrower Operations |
||
| 101 |
√
|
creationCode = type(BorrowerOperations).creationCode; |
|
| 102 |
args = abi.encode( |
||
| 103 |
√
|
addr.cdpManagerAddress, |
|
| 104 |
√
|
addr.activePoolAddress, |
|
| 105 |
√
|
addr.collSurplusPoolAddress, |
|
| 106 |
√
|
addr.priceFeedAddress, |
|
| 107 |
√
|
addr.sortedCdpsAddress, |
|
| 108 |
√
|
addr.ebtcTokenAddress, |
|
| 109 |
√
|
addr.feeRecipientAddress, |
|
| 110 |
√
|
address(collateral) |
|
| 111 |
); |
||
| 112 | |||
| 113 |
√
|
borrowerOperations = BorrowerOperations( |
|
| 114 |
√
|
ebtcDeployer.deploy( |
|
| 115 |
√
|
ebtcDeployer.BORROWER_OPERATIONS(), |
|
| 116 |
√
|
abi.encodePacked(creationCode, args) |
|
| 117 |
) |
||
| 118 |
); |
||
| 119 | |||
| 120 |
// Price Feed Mock |
||
| 121 |
√
|
creationCode = type(PriceFeedTestnet).creationCode; |
|
| 122 |
√
|
args = abi.encode(addr.authorityAddress); |
|
| 123 | |||
| 124 |
√
|
priceFeedMock = PriceFeedTestnet( |
|
| 125 |
√
|
ebtcDeployer.deploy(ebtcDeployer.PRICE_FEED(), abi.encodePacked(creationCode, args)) |
|
| 126 |
); |
||
| 127 | |||
| 128 |
// Sorted CDPS |
||
| 129 |
√
|
creationCode = type(SortedCdps).creationCode; |
|
| 130 |
args = abi.encode( |
||
| 131 |
√
|
type(uint256).max, |
|
| 132 |
√
|
addr.cdpManagerAddress, |
|
| 133 |
√
|
addr.borrowerOperationsAddress |
|
| 134 |
); |
||
| 135 | |||
| 136 |
√
|
sortedCdps = SortedCdps( |
|
| 137 |
√
|
ebtcDeployer.deploy(ebtcDeployer.SORTED_CDPS(), abi.encodePacked(creationCode, args)) |
|
| 138 |
); |
||
| 139 | |||
| 140 |
// Active Pool |
||
| 141 |
√
|
creationCode = type(ActivePool).creationCode; |
|
| 142 |
args = abi.encode( |
||
| 143 |
√
|
addr.borrowerOperationsAddress, |
|
| 144 |
√
|
addr.cdpManagerAddress, |
|
| 145 |
√
|
address(collateral), |
|
| 146 |
√
|
addr.collSurplusPoolAddress, |
|
| 147 |
√
|
addr.feeRecipientAddress |
|
| 148 |
); |
||
| 149 | |||
| 150 |
√
|
activePool = ActivePool( |
|
| 151 |
√
|
ebtcDeployer.deploy(ebtcDeployer.ACTIVE_POOL(), abi.encodePacked(creationCode, args)) |
|
| 152 |
); |
||
| 153 | |||
| 154 |
// Coll Surplus Pool |
||
| 155 |
√
|
creationCode = type(CollSurplusPool).creationCode; |
|
| 156 |
args = abi.encode( |
||
| 157 |
√
|
addr.borrowerOperationsAddress, |
|
| 158 |
√
|
addr.cdpManagerAddress, |
|
| 159 |
√
|
addr.activePoolAddress, |
|
| 160 |
√
|
address(collateral) |
|
| 161 |
); |
||
| 162 | |||
| 163 |
√
|
collSurplusPool = CollSurplusPool( |
|
| 164 |
√
|
ebtcDeployer.deploy( |
|
| 165 |
√
|
ebtcDeployer.COLL_SURPLUS_POOL(), |
|
| 166 |
√
|
abi.encodePacked(creationCode, args) |
|
| 167 |
) |
||
| 168 |
); |
||
| 169 | |||
| 170 |
// Hint Helpers |
||
| 171 |
√
|
creationCode = type(HintHelpers).creationCode; |
|
| 172 |
args = abi.encode( |
||
| 173 |
√
|
addr.sortedCdpsAddress, |
|
| 174 |
√
|
addr.cdpManagerAddress, |
|
| 175 |
√
|
address(collateral), |
|
| 176 |
√
|
addr.activePoolAddress, |
|
| 177 |
√
|
addr.priceFeedAddress |
|
| 178 |
); |
||
| 179 | |||
| 180 |
√
|
hintHelpers = HintHelpers( |
|
| 181 |
√
|
ebtcDeployer.deploy( |
|
| 182 |
√
|
ebtcDeployer.HINT_HELPERS(), |
|
| 183 |
√
|
abi.encodePacked(creationCode, args) |
|
| 184 |
) |
||
| 185 |
); |
||
| 186 | |||
| 187 |
// eBTC Token |
||
| 188 |
√
|
creationCode = type(EBTCTokenTester).creationCode; |
|
| 189 |
args = abi.encode( |
||
| 190 |
√
|
addr.cdpManagerAddress, |
|
| 191 |
√
|
addr.borrowerOperationsAddress, |
|
| 192 |
√
|
addr.authorityAddress |
|
| 193 |
); |
||
| 194 | |||
| 195 |
√
|
eBTCToken = EBTCTokenTester( |
|
| 196 |
√
|
ebtcDeployer.deploy(ebtcDeployer.EBTC_TOKEN(), abi.encodePacked(creationCode, args)) |
|
| 197 |
); |
||
| 198 | |||
| 199 |
// Fee Recipieint |
||
| 200 |
√
|
creationCode = type(FeeRecipient).creationCode; |
|
| 201 |
args = abi.encode( |
||
| 202 |
√
|
addr.ebtcTokenAddress, |
|
| 203 |
√
|
addr.cdpManagerAddress, |
|
| 204 |
√
|
addr.borrowerOperationsAddress, |
|
| 205 |
√
|
addr.activePoolAddress, |
|
| 206 |
√
|
address(collateral) |
|
| 207 |
); |
||
| 208 | |||
| 209 |
√
|
feeRecipient = FeeRecipient( |
|
| 210 |
√
|
ebtcDeployer.deploy( |
|
| 211 |
√
|
ebtcDeployer.FEE_RECIPIENT(), |
|
| 212 |
√
|
abi.encodePacked(creationCode, args) |
|
| 213 |
) |
||
| 214 |
); |
||
| 215 | |||
| 216 |
// Configure authority |
||
| 217 |
√
|
authority.setRoleName(0, "Admin"); |
|
| 218 |
√
|
authority.setRoleName(1, "eBTCToken: mint"); |
|
| 219 |
√
|
authority.setRoleName(2, "eBTCToken: burn"); |
|
| 220 |
√
|
authority.setRoleName(3, "CDPManager: all"); |
|
| 221 |
√
|
authority.setRoleName(4, "PriceFeed: setTellorCaller"); |
|
| 222 |
√
|
authority.setRoleName(5, "BorrowerOperations: all"); |
|
| 223 | |||
| 224 |
√
|
authority.setRoleCapability(1, address(eBTCToken), eBTCToken.mint.selector, true); |
|
| 225 | |||
| 226 |
√
|
authority.setRoleCapability(2, address(eBTCToken), BURN_SIG, true); |
|
| 227 | |||
| 228 |
√
|
authority.setRoleCapability( |
|
| 229 |
√
|
3, |
|
| 230 |
√
|
address(cdpManager), |
|
| 231 |
√
|
cdpManager.setStakingRewardSplit.selector, |
|
| 232 |
√
|
true |
|
| 233 |
); |
||
| 234 |
√
|
authority.setRoleCapability( |
|
| 235 |
√
|
3, |
|
| 236 |
√
|
address(cdpManager), |
|
| 237 |
√
|
cdpManager.setRedemptionFeeFloor.selector, |
|
| 238 |
√
|
true |
|
| 239 |
); |
||
| 240 |
√
|
authority.setRoleCapability( |
|
| 241 |
√
|
3, |
|
| 242 |
√
|
address(cdpManager), |
|
| 243 |
√
|
cdpManager.setMinuteDecayFactor.selector, |
|
| 244 |
√
|
true |
|
| 245 |
); |
||
| 246 |
√
|
authority.setRoleCapability(3, address(cdpManager), cdpManager.setBeta.selector, true); |
|
| 247 |
√
|
authority.setRoleCapability( |
|
| 248 |
√
|
3, |
|
| 249 |
√
|
address(cdpManager), |
|
| 250 |
√
|
cdpManager.setGracePeriod.selector, |
|
| 251 |
√
|
true |
|
| 252 |
); |
||
| 253 |
√
|
authority.setRoleCapability( |
|
| 254 |
√
|
3, |
|
| 255 |
√
|
address(cdpManager), |
|
| 256 |
√
|
cdpManager.setRedemptionsPaused.selector, |
|
| 257 |
√
|
true |
|
| 258 |
); |
||
| 259 | |||
| 260 |
√
|
authority.setRoleCapability( |
|
| 261 |
√
|
4, |
|
| 262 |
√
|
address(priceFeedMock), |
|
| 263 |
√
|
priceFeedMock.setFallbackCaller.selector, |
|
| 264 |
√
|
true |
|
| 265 |
); |
||
| 266 | |||
| 267 |
√
|
authority.setRoleCapability( |
|
| 268 |
√
|
5, |
|
| 269 |
√
|
address(borrowerOperations), |
|
| 270 |
√
|
borrowerOperations.setFeeBps.selector, |
|
| 271 |
√
|
true |
|
| 272 |
); |
||
| 273 |
√
|
authority.setRoleCapability( |
|
| 274 |
√
|
5, |
|
| 275 |
√
|
address(borrowerOperations), |
|
| 276 |
√
|
borrowerOperations.setFlashLoansPaused.selector, |
|
| 277 |
√
|
true |
|
| 278 |
); |
||
| 279 | |||
| 280 |
√
|
authority.setRoleCapability(5, address(activePool), activePool.setFeeBps.selector, true); |
|
| 281 |
√
|
authority.setRoleCapability( |
|
| 282 |
√
|
5, |
|
| 283 |
√
|
address(activePool), |
|
| 284 |
√
|
activePool.setFlashLoansPaused.selector, |
|
| 285 |
√
|
true |
|
| 286 |
); |
||
| 287 |
√
|
authority.setRoleCapability( |
|
| 288 |
√
|
5, |
|
| 289 |
√
|
address(activePool), |
|
| 290 |
√
|
activePool.claimFeeRecipientCollShares.selector, |
|
| 291 |
√
|
true |
|
| 292 |
); |
||
| 293 | |||
| 294 |
√
|
authority.setUserRole(defaultGovernance, 0, true); |
|
| 295 |
√
|
authority.setUserRole(defaultGovernance, 1, true); |
|
| 296 |
√
|
authority.setUserRole(defaultGovernance, 2, true); |
|
| 297 |
√
|
authority.setUserRole(defaultGovernance, 3, true); |
|
| 298 |
√
|
authority.setUserRole(defaultGovernance, 4, true); |
|
| 299 |
√
|
authority.setUserRole(defaultGovernance, 5, true); |
|
| 300 | |||
| 301 |
√
|
crLens = new CRLens(address(cdpManager), address(priceFeedMock)); |
|
| 302 | |||
| 303 |
√
|
liquidationSequencer = new LiquidationSequencer( |
|
| 304 |
√
|
address(cdpManager), |
|
| 305 |
√
|
address(cdpManager.sortedCdps()), |
|
| 306 |
√
|
address(priceFeedMock), |
|
| 307 |
√
|
address(activePool), |
|
| 308 |
√
|
address(collateral) |
|
| 309 |
); |
||
| 310 |
√
|
syncedLiquidationSequencer = new SyncedLiquidationSequencer( |
|
| 311 |
√
|
address(cdpManager), |
|
| 312 |
√
|
address(cdpManager.sortedCdps()), |
|
| 313 |
√
|
address(priceFeedMock), |
|
| 314 |
√
|
address(activePool), |
|
| 315 |
√
|
address(collateral) |
|
| 316 |
); |
||
| 317 |
} |
||
| 318 |
} |
||
| 319 | |||
| 320 |
event Log(string); |
||
| 321 | |||
| 322 |
function _setUpFork() internal {
|
||
| 323 |
defaultGovernance = address(0xA967Ba66Fb284EC18bbe59f65bcf42dD11BA8128); |
||
| 324 |
ebtcDeployer = EBTCDeployer(0xe90f99c08F286c48db4D1AfdAE6C122de69B7219); |
||
| 325 |
collateral = CollateralTokenTester(payable(0xf8017430A0efE03577f6aF88069a21900448A373)); |
||
| 326 |
{
|
||
| 327 |
authority = Governor(0x4945Fc25282b1bC103d2C62C251Cd022138c1de9); |
||
| 328 |
liqudationLibrary = LiquidationLibrary(0xE8943a17363DE9A6e0d4A5d48d5Ab45283199F77); |
||
| 329 |
cdpManager = CdpManager(0x0c5C2B93b96C9B3aD7fb9915952BD7BA256C4f04); |
||
| 330 |
borrowerOperations = BorrowerOperations(0xA178BFBc42E3D886d540CDDcf4562c53a8Fc02c1); |
||
| 331 |
priceFeedMock = PriceFeedTestnet(0x5C819E5D61EFCfBd7e4635f1112f3bF94663999b); |
||
| 332 |
sortedCdps = SortedCdps(0xDeFF25eC3cd3041BC8B9A464F9BEc12EB8247Be6); |
||
| 333 |
activePool = ActivePool(0x55abdfb760dd032627D531f7cF3DAa72549CEbA2); |
||
| 334 |
collSurplusPool = CollSurplusPool(0x7b4D951D7b8090f62bD009b371abd7Fe04aB7e1A); |
||
| 335 |
hintHelpers = HintHelpers(0xCaBdBc4218dd4b9E3fB9842232aD0aFc7c431693); |
||
| 336 |
eBTCToken = EBTCTokenTester(0x9Aa69Db8c53E504EF22615390EE9Eb72cb8bE498); |
||
| 337 |
feeRecipient = FeeRecipient(0x40FF68eaE525233950B63C2BCEa39770efDE52A4); |
||
| 338 | |||
| 339 |
crLens = new CRLens(address(cdpManager), address(priceFeedMock)); |
||
| 340 | |||
| 341 |
liquidationSequencer = new LiquidationSequencer( |
||
| 342 |
address(cdpManager), |
||
| 343 |
address(cdpManager.sortedCdps()), |
||
| 344 |
address(priceFeedMock), |
||
| 345 |
address(activePool), |
||
| 346 |
address(collateral) |
||
| 347 |
); |
||
| 348 |
} |
||
| 349 |
} |
||
| 350 | |||
| 351 |
function _setUpActors() internal {
|
||
| 352 |
√
|
bool success; |
|
| 353 |
√
|
address[] memory tokens = new address[](2); |
|
| 354 |
√
|
tokens[0] = address(eBTCToken); |
|
| 355 |
√
|
tokens[1] = address(collateral); |
|
| 356 |
√
|
address[] memory callers = new address[](2); |
|
| 357 |
√
|
callers[0] = address(borrowerOperations); |
|
| 358 |
√
|
callers[1] = address(activePool); |
|
| 359 |
√
|
address[] memory addresses = new address[](3); |
|
| 360 |
√
|
addresses[0] = USER1; |
|
| 361 |
√
|
addresses[1] = USER2; |
|
| 362 |
√
|
addresses[2] = USER3; |
|
| 363 |
√
|
Actor[] memory actorsArray = new Actor[](NUMBER_OF_ACTORS); |
|
| 364 |
√
|
for (uint i = 0; i < NUMBER_OF_ACTORS; i++) {
|
|
| 365 |
√
|
actors[addresses[i]] = new Actor(tokens, callers); |
|
| 366 |
√
|
(success, ) = address(actors[addresses[i]]).call{value: INITIAL_ETH_BALANCE}("");
|
|
| 367 |
√
|
assert(success); |
|
| 368 |
√
|
(success, ) = actors[addresses[i]].proxy( |
|
| 369 |
√
|
address(collateral), |
|
| 370 |
√
|
abi.encodeWithSelector(CollateralTokenTester.deposit.selector, ""), |
|
| 371 |
INITIAL_COLL_BALANCE |
||
| 372 |
); |
||
| 373 |
√
|
assert(success); |
|
| 374 |
√
|
actorsArray[i] = actors[addresses[i]]; |
|
| 375 |
} |
||
| 376 |
√
|
simulator = new Simulator(actorsArray, cdpManager, sortedCdps, borrowerOperations); |
|
| 377 |
} |
||
| 378 | |||
| 379 |
function _openWhaleCdpAndTransferEBTC() internal {
|
||
| 380 |
bool success; |
||
| 381 |
Actor actor = actors[USER3]; // USER3 is the whale CDP holder |
||
| 382 |
uint256 _col = INITIAL_COLL_BALANCE / 2; // 50% of their initial collateral balance |
||
| 383 | |||
| 384 |
uint256 price = priceFeedMock.getPrice(); |
||
| 385 |
uint256 _EBTCAmount = (_col * price) / cdpManager.CCR(); |
||
| 386 | |||
| 387 |
(success, ) = actor.proxy( |
||
| 388 |
address(collateral), |
||
| 389 |
abi.encodeWithSelector( |
||
| 390 |
CollateralTokenTester.approve.selector, |
||
| 391 |
address(borrowerOperations), |
||
| 392 |
_col |
||
| 393 |
) |
||
| 394 |
); |
||
| 395 |
assert(success); |
||
| 396 |
(success, ) = actor.proxy( |
||
| 397 |
address(borrowerOperations), |
||
| 398 |
abi.encodeWithSelector( |
||
| 399 |
BorrowerOperations.openCdp.selector, |
||
| 400 |
_EBTCAmount, |
||
| 401 |
bytes32(0), |
||
| 402 |
bytes32(0), |
||
| 403 |
_col |
||
| 404 |
) |
||
| 405 |
); |
||
| 406 |
assert(success); |
||
| 407 |
address[] memory addresses = new address[](2); |
||
| 408 |
addresses[0] = USER1; |
||
| 409 |
addresses[1] = USER2; |
||
| 410 |
for (uint i = 0; i < addresses.length; i++) {
|
||
| 411 |
(success, ) = actor.proxy( |
||
| 412 |
address(eBTCToken), |
||
| 413 |
abi.encodeWithSelector(eBTCToken.transfer.selector, actors[addresses[i]], _EBTCAmount / 3) |
||
| 414 |
); |
||
| 415 |
assert(success); |
||
| 416 |
} |
||
| 417 |
} |
||
| 418 |
} |
||
| 419 |
| Lines covered: | 390 / 593 (65.8%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "@crytic/properties/contracts/util/Hevm.sol"; |
||
| 6 | |||
| 7 |
import "../../Interfaces/ICdpManagerData.sol"; |
||
| 8 |
import "../../Dependencies/SafeMath.sol"; |
||
| 9 |
import "../../CdpManager.sol"; |
||
| 10 |
import "../../LiquidationLibrary.sol"; |
||
| 11 |
import "../../BorrowerOperations.sol"; |
||
| 12 |
import "../../ActivePool.sol"; |
||
| 13 |
import "../../CollSurplusPool.sol"; |
||
| 14 |
import "../../SortedCdps.sol"; |
||
| 15 |
import "../../HintHelpers.sol"; |
||
| 16 |
import "../../FeeRecipient.sol"; |
||
| 17 |
import "../testnet/PriceFeedTestnet.sol"; |
||
| 18 |
import "../CollateralTokenTester.sol"; |
||
| 19 |
import "../EBTCTokenTester.sol"; |
||
| 20 |
import "../../Governor.sol"; |
||
| 21 |
import "../../EBTCDeployer.sol"; |
||
| 22 | |||
| 23 |
import "./Properties.sol"; |
||
| 24 |
import "./Actor.sol"; |
||
| 25 |
import "./BeforeAfter.sol"; |
||
| 26 |
import "./TargetContractSetup.sol"; |
||
| 27 |
import "./Asserts.sol"; |
||
| 28 |
import "../BaseStorageVariables.sol"; |
||
| 29 | |||
| 30 |
abstract contract TargetFunctions is Properties {
|
||
| 31 |
modifier setup() virtual {
|
||
| 32 |
√
|
⟳
|
actor = actors[msg.sender]; |
| 33 |
_; |
||
| 34 |
} |
||
| 35 | |||
| 36 |
/////////////////////////////////////////////////////// |
||
| 37 |
// Helper functions |
||
| 38 |
/////////////////////////////////////////////////////// |
||
| 39 | |||
| 40 |
function _totalCdpsBelowMcr() internal returns (uint256) {
|
||
| 41 |
uint256 ans; |
||
| 42 |
bytes32 currentCdp = sortedCdps.getFirst(); |
||
| 43 | |||
| 44 |
uint256 _price = priceFeedMock.getPrice(); |
||
| 45 | |||
| 46 |
while (currentCdp != bytes32(0)) {
|
||
| 47 |
if (cdpManager.getICR(currentCdp, _price) < cdpManager.MCR()) {
|
||
| 48 |
++ans; |
||
| 49 |
} |
||
| 50 | |||
| 51 |
currentCdp = sortedCdps.getNext(currentCdp); |
||
| 52 |
} |
||
| 53 | |||
| 54 |
return ans; |
||
| 55 |
} |
||
| 56 | |||
| 57 |
√
|
function _getCdpIdsAndICRs() internal view returns (Cdp[] memory ans) {
|
|
| 58 |
√
|
ans = new Cdp[](sortedCdps.getSize()); |
|
| 59 |
√
|
uint256 i = 0; |
|
| 60 |
√
|
bytes32 currentCdp = sortedCdps.getFirst(); |
|
| 61 | |||
| 62 |
√
|
uint256 _price = priceFeedMock.getPrice(); |
|
| 63 | |||
| 64 |
√
|
while (currentCdp != bytes32(0)) {
|
|
| 65 |
√
|
ans[i++] = Cdp({id: currentCdp, icr: cdpManager.getSyncedICR(currentCdp, _price)}); /// @audit NOTE: Synced to ensure it's realistic
|
|
| 66 | |||
| 67 |
√
|
currentCdp = sortedCdps.getNext(currentCdp); |
|
| 68 |
} |
||
| 69 |
} |
||
| 70 | |||
| 71 |
function _cdpIdsAndICRsDiff( |
||
| 72 |
Cdp[] memory superset, |
||
| 73 |
Cdp[] memory subset |
||
| 74 |
) internal returns (Cdp[] memory ans) {
|
||
| 75 |
ans = new Cdp[](superset.length - subset.length); |
||
| 76 |
uint256 index = 0; |
||
| 77 |
for (uint256 i = 0; i < superset.length; i++) {
|
||
| 78 |
bool duplicate = false; |
||
| 79 |
for (uint256 j = 0; j < subset.length; j++) {
|
||
| 80 |
if (superset[i].id == subset[j].id) {
|
||
| 81 |
duplicate = true; |
||
| 82 |
} |
||
| 83 |
} |
||
| 84 |
if (!duplicate) {
|
||
| 85 |
ans[index++] = superset[i]; |
||
| 86 |
} |
||
| 87 |
} |
||
| 88 |
} |
||
| 89 | |||
| 90 |
√
|
function _getRandomCdp(uint _i) internal view returns (bytes32) {
|
|
| 91 |
√
|
uint _cdpIdx = _i % cdpManager.getActiveCdpsCount(); |
|
| 92 |
√
|
return cdpManager.CdpIds(_cdpIdx); |
|
| 93 |
} |
||
| 94 | |||
| 95 |
event FlashLoanAction(uint, uint); |
||
| 96 | |||
| 97 |
function _getFlashLoanActions(uint256 value) internal returns (bytes memory) {
|
||
| 98 |
uint256 _actions = between(value, 1, MAX_FLASHLOAN_ACTIONS); |
||
| 99 |
uint256 _EBTCAmount = between(value, 1, eBTCToken.totalSupply() / 2); |
||
| 100 |
uint256 _col = between(value, 1, cdpManager.getSystemCollShares() / 2); |
||
| 101 |
uint256 _n = between(value, 1, cdpManager.getActiveCdpsCount()); |
||
| 102 | |||
| 103 |
uint256 numberOfCdps = sortedCdps.cdpCountOf(address(actor)); |
||
| 104 |
require(numberOfCdps > 0, "Actor must have at least one CDP open"); |
||
| 105 |
uint256 _i = between(value, 0, numberOfCdps - 1); |
||
| 106 |
bytes32 _cdpId = sortedCdps.cdpOfOwnerByIndex(address(actor), _i); |
||
| 107 |
t(_cdpId != bytes32(0), "CDP ID must not be null if the index is valid"); |
||
| 108 | |||
| 109 |
address[] memory _targets = new address[](_actions); |
||
| 110 |
bytes[] memory _calldatas = new bytes[](_actions); |
||
| 111 | |||
| 112 |
address[] memory _allTargets = new address[](6); |
||
| 113 |
bytes[] memory _allCalldatas = new bytes[](6); |
||
| 114 | |||
| 115 |
_allTargets[0] = address(borrowerOperations); |
||
| 116 |
_allCalldatas[0] = abi.encodeWithSelector( |
||
| 117 |
BorrowerOperations.openCdp.selector, |
||
| 118 |
_EBTCAmount, |
||
| 119 |
bytes32(0), |
||
| 120 |
bytes32(0), |
||
| 121 |
_col |
||
| 122 |
); |
||
| 123 | |||
| 124 |
_allTargets[1] = address(borrowerOperations); |
||
| 125 |
_allCalldatas[1] = abi.encodeWithSelector(BorrowerOperations.closeCdp.selector, _cdpId); |
||
| 126 | |||
| 127 |
_allTargets[2] = address(borrowerOperations); |
||
| 128 |
_allCalldatas[2] = abi.encodeWithSelector( |
||
| 129 |
BorrowerOperations.addColl.selector, |
||
| 130 |
_cdpId, |
||
| 131 |
_cdpId, |
||
| 132 |
_cdpId, |
||
| 133 |
_col |
||
| 134 |
); |
||
| 135 | |||
| 136 |
_allTargets[3] = address(borrowerOperations); |
||
| 137 |
_allCalldatas[3] = abi.encodeWithSelector( |
||
| 138 |
BorrowerOperations.withdrawColl.selector, |
||
| 139 |
_cdpId, |
||
| 140 |
_col, |
||
| 141 |
_cdpId, |
||
| 142 |
_cdpId |
||
| 143 |
); |
||
| 144 | |||
| 145 |
_allTargets[4] = address(borrowerOperations); |
||
| 146 |
_allCalldatas[4] = abi.encodeWithSelector( |
||
| 147 |
BorrowerOperations.withdrawDebt.selector, |
||
| 148 |
_cdpId, |
||
| 149 |
_EBTCAmount, |
||
| 150 |
_cdpId, |
||
| 151 |
_cdpId |
||
| 152 |
); |
||
| 153 | |||
| 154 |
_allTargets[5] = address(borrowerOperations); |
||
| 155 |
_allCalldatas[5] = abi.encodeWithSelector( |
||
| 156 |
BorrowerOperations.repayDebt.selector, |
||
| 157 |
_cdpId, |
||
| 158 |
_EBTCAmount, |
||
| 159 |
_cdpId, |
||
| 160 |
_cdpId |
||
| 161 |
); |
||
| 162 | |||
| 163 |
for (uint256 _j = 0; _j < _actions; ++_j) {
|
||
| 164 |
_i = uint256(keccak256(abi.encodePacked(value, _j, _i))) % _allTargets.length; |
||
| 165 |
emit FlashLoanAction(_j, _i); |
||
| 166 | |||
| 167 |
_targets[_j] = _allTargets[_i]; |
||
| 168 |
_calldatas[_j] = _allCalldatas[_i]; |
||
| 169 |
} |
||
| 170 | |||
| 171 |
return abi.encode(_targets, _calldatas); |
||
| 172 |
} |
||
| 173 | |||
| 174 |
⟳
|
function _getFirstCdpWithIcrGteMcr() internal returns (bytes32) {
|
|
| 175 |
⟳
|
bytes32 _cId = sortedCdps.getLast(); |
|
| 176 |
⟳
|
address currentBorrower = sortedCdps.getOwnerAddress(_cId); |
|
| 177 |
// Find the first cdp with ICR >= MCR |
||
| 178 |
while ( |
||
| 179 |
⟳
|
currentBorrower != address(0) && |
|
| 180 |
⟳
|
cdpManager.getICR(_cId, priceFeedMock.getPrice()) < cdpManager.MCR() |
|
| 181 |
) {
|
||
| 182 |
⟳
|
_cId = sortedCdps.getPrev(_cId); |
|
| 183 |
⟳
|
currentBorrower = sortedCdps.getOwnerAddress(_cId); |
|
| 184 |
} |
||
| 185 |
⟳
|
return _cId; |
|
| 186 |
} |
||
| 187 | |||
| 188 |
function _atLeastOneCdpIsLiquidatable( |
||
| 189 |
Cdp[] memory cdps, |
||
| 190 |
bool isRecoveryModeBefore |
||
| 191 |
√
|
) internal view returns (bool atLeastOneCdpIsLiquidatable) {
|
|
| 192 |
√
|
for (uint256 i = 0; i < cdps.length; ++i) {
|
|
| 193 |
if ( |
||
| 194 |
√
|
cdps[i].icr < cdpManager.MCR() || |
|
| 195 |
√
|
(cdps[i].icr < cdpManager.CCR() && isRecoveryModeBefore) |
|
| 196 |
) {
|
||
| 197 |
atLeastOneCdpIsLiquidatable = true; |
||
| 198 |
break; |
||
| 199 |
} |
||
| 200 |
} |
||
| 201 |
} |
||
| 202 | |||
| 203 |
/////////////////////////////////////////////////////// |
||
| 204 |
// CdpManager |
||
| 205 |
/////////////////////////////////////////////////////// |
||
| 206 | |||
| 207 |
function liquidate(uint _i) public setup {
|
||
| 208 |
√
|
⟳
|
bool success; |
| 209 |
√
|
⟳
|
bytes memory returnData; |
| 210 | |||
| 211 |
√
|
⟳
|
require(cdpManager.getActiveCdpsCount() > 1, "Cannot liquidate last CDP"); |
| 212 | |||
| 213 |
√
|
bytes32 _cdpId = _getRandomCdp(_i); |
|
| 214 | |||
| 215 |
√
|
(uint256 entireDebt, , ) = cdpManager.getDebtAndCollShares(_cdpId); |
|
| 216 |
√
|
require(entireDebt > 0, "CDP must have debt"); |
|
| 217 | |||
| 218 |
√
|
_before(_cdpId); |
|
| 219 | |||
| 220 |
√
|
(success, returnData) = actor.proxy( |
|
| 221 |
√
|
address(cdpManager), |
|
| 222 |
√
|
abi.encodeWithSelector(CdpManager.liquidate.selector, _cdpId) |
|
| 223 |
); |
||
| 224 | |||
| 225 |
√
|
_after(_cdpId); |
|
| 226 | |||
| 227 |
√
|
if (success) {
|
|
| 228 |
if ( |
||
| 229 |
vars.newIcrBefore >= cdpManager.LICR() // 103% else liquidating locks in bad debt |
||
| 230 |
) {
|
||
| 231 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/5 |
||
| 232 |
gte(vars.newTcrAfter, vars.newTcrBefore, L_12); |
||
| 233 |
} |
||
| 234 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/12 |
||
| 235 |
t( |
||
| 236 |
vars.newIcrBefore < cdpManager.MCR() || |
||
| 237 |
(vars.newIcrBefore < cdpManager.CCR() && vars.isRecoveryModeBefore), |
||
| 238 |
L_01 |
||
| 239 |
); |
||
| 240 |
if ( |
||
| 241 |
vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 242 |
vars.isRecoveryModeBefore && |
||
| 243 |
vars.isRecoveryModeAfter |
||
| 244 |
) {
|
||
| 245 |
eq( |
||
| 246 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 247 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 248 |
L_14 |
||
| 249 |
); |
||
| 250 |
} |
||
| 251 | |||
| 252 |
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
||
| 253 |
t( |
||
| 254 |
!vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 255 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 256 |
L_15 |
||
| 257 |
); |
||
| 258 |
} |
||
| 259 | |||
| 260 |
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
||
| 261 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 262 |
} |
||
| 263 | |||
| 264 |
gte( |
||
| 265 |
vars.actorCollAfter, |
||
| 266 |
vars.actorCollBefore + |
||
| 267 |
collateral.getPooledEthByShares(vars.liquidatorRewardSharesBefore), |
||
| 268 |
L_09 |
||
| 269 |
); |
||
| 270 |
√
|
} else if (vars.sortedCdpsSizeBefore > _i) {
|
|
| 271 |
√
|
assertRevertReasonNotEqual(returnData, "Panic(17)"); |
|
| 272 |
} |
||
| 273 |
} |
||
| 274 | |||
| 275 |
function partialLiquidate(uint _i, uint _partialAmount) public setup {
|
||
| 276 |
√
|
⟳
|
bool success; |
| 277 |
√
|
⟳
|
bytes memory returnData; |
| 278 | |||
| 279 |
√
|
⟳
|
require(cdpManager.getActiveCdpsCount() > 1, "Cannot liquidate last CDP"); |
| 280 | |||
| 281 |
√
|
bytes32 _cdpId = _getRandomCdp(_i); |
|
| 282 | |||
| 283 |
√
|
(uint256 entireDebt, , ) = cdpManager.getDebtAndCollShares(_cdpId); |
|
| 284 |
√
|
require(entireDebt > 0, "CDP must have debt"); |
|
| 285 | |||
| 286 |
√
|
_partialAmount = between(_partialAmount, 0, entireDebt); |
|
| 287 | |||
| 288 |
√
|
_before(_cdpId); |
|
| 289 | |||
| 290 |
√
|
(success, returnData) = actor.proxy( |
|
| 291 |
√
|
address(cdpManager), |
|
| 292 |
abi.encodeWithSelector( |
||
| 293 |
√
|
CdpManager.partiallyLiquidate.selector, |
|
| 294 |
√
|
_cdpId, |
|
| 295 |
√
|
_partialAmount, |
|
| 296 |
√
|
_cdpId, |
|
| 297 |
√
|
_cdpId |
|
| 298 |
) |
||
| 299 |
); |
||
| 300 | |||
| 301 |
√
|
_after(_cdpId); |
|
| 302 | |||
| 303 |
√
|
if (success) {
|
|
| 304 |
√
|
lt(vars.cdpDebtAfter, vars.cdpDebtBefore, "Partial liquidation must reduce CDP debt"); |
|
| 305 | |||
| 306 |
if ( |
||
| 307 |
√
|
vars.newIcrBefore >= cdpManager.LICR() // 103% else liquidating locks in bad debt |
|
| 308 |
) {
|
||
| 309 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/5 |
||
| 310 |
√
|
gte(vars.newTcrAfter, vars.newTcrBefore, L_12); |
|
| 311 |
} |
||
| 312 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/12 |
||
| 313 |
√
|
t( |
|
| 314 |
√
|
vars.newIcrBefore < cdpManager.MCR() || |
|
| 315 |
(vars.newIcrBefore < cdpManager.CCR() && vars.isRecoveryModeBefore), |
||
| 316 |
√
|
L_01 |
|
| 317 |
); |
||
| 318 | |||
| 319 |
√
|
eq(vars.sortedCdpsSizeAfter, vars.sortedCdpsSizeBefore, "L-17 : Partial Liquidations do not close Cdps"); |
|
| 320 | |||
| 321 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/4 |
||
| 322 |
√
|
if (vars.sortedCdpsSizeAfter == vars.sortedCdpsSizeBefore) {
|
|
| 323 |
// CDP was not fully liquidated |
||
| 324 |
√
|
gte( |
|
| 325 |
√
|
collateral.getPooledEthByShares(cdpManager.getCdpCollShares(_cdpId)), |
|
| 326 |
√
|
borrowerOperations.MIN_NET_COLL(), |
|
| 327 |
√
|
GENERAL_10 |
|
| 328 |
); |
||
| 329 |
} |
||
| 330 | |||
| 331 |
if ( |
||
| 332 |
√
|
vars.lastGracePeriodStartTimestampIsSetBefore && |
|
| 333 |
vars.isRecoveryModeBefore && |
||
| 334 |
vars.isRecoveryModeAfter |
||
| 335 |
) {
|
||
| 336 |
eq( |
||
| 337 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 338 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 339 |
L_14 |
||
| 340 |
); |
||
| 341 |
} |
||
| 342 | |||
| 343 |
√
|
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
|
| 344 |
t( |
||
| 345 |
!vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 346 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 347 |
L_15 |
||
| 348 |
); |
||
| 349 |
} |
||
| 350 | |||
| 351 |
√
|
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
|
| 352 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 353 |
} |
||
| 354 |
} else {
|
||
| 355 |
√
|
assertRevertReasonNotEqual(returnData, "Panic(17)"); |
|
| 356 |
} |
||
| 357 |
} |
||
| 358 | |||
| 359 |
function liquidateCdps(uint _n) public setup {
|
||
| 360 |
√
|
⟳
|
bool success; |
| 361 |
√
|
⟳
|
bytes memory returnData; |
| 362 | |||
| 363 |
√
|
⟳
|
require(cdpManager.getActiveCdpsCount() > 1, "Cannot liquidate last CDP"); |
| 364 | |||
| 365 |
√
|
_n = between(_n, 1, cdpManager.getActiveCdpsCount()); |
|
| 366 | |||
| 367 |
√
|
Cdp[] memory cdpsBefore = _getCdpIdsAndICRs(); |
|
| 368 | |||
| 369 |
√
|
_before(bytes32(0)); |
|
| 370 | |||
| 371 |
√
|
bytes32[] memory batch = liquidationSequencer.sequenceLiqToBatchLiqWithPrice( |
|
| 372 |
√
|
_n, |
|
| 373 |
√
|
vars.priceBefore |
|
| 374 |
); |
||
| 375 | |||
| 376 |
√
|
(success, returnData) = actor.proxy( |
|
| 377 |
√
|
address(cdpManager), |
|
| 378 |
√
|
abi.encodeWithSelector(CdpManager.batchLiquidateCdps.selector, batch) |
|
| 379 |
); |
||
| 380 | |||
| 381 |
√
|
_after(bytes32(0)); |
|
| 382 | |||
| 383 |
√
|
if (success) {
|
|
| 384 |
Cdp[] memory cdpsAfter = _getCdpIdsAndICRs(); |
||
| 385 | |||
| 386 |
Cdp[] memory cdpsLiquidated = _cdpIdsAndICRsDiff(cdpsBefore, cdpsAfter); |
||
| 387 |
gte( |
||
| 388 |
cdpsLiquidated.length, |
||
| 389 |
1, |
||
| 390 |
"liquidateCdps must liquidate at least 1 CDP when successful" |
||
| 391 |
); |
||
| 392 |
lte(cdpsLiquidated.length, _n, "liquidateCdps must not liquidate more than n CDPs"); |
||
| 393 |
for (uint256 i = 0; i < cdpsLiquidated.length; ++i) {
|
||
| 394 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/12 |
||
| 395 |
t( |
||
| 396 |
cdpsLiquidated[i].icr < cdpManager.MCR() || |
||
| 397 |
(cdpsLiquidated[i].icr < cdpManager.CCR() && vars.isRecoveryModeBefore), |
||
| 398 |
L_01 |
||
| 399 |
); |
||
| 400 |
} |
||
| 401 | |||
| 402 |
if ( |
||
| 403 |
vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 404 |
vars.isRecoveryModeBefore && |
||
| 405 |
vars.isRecoveryModeAfter |
||
| 406 |
) {
|
||
| 407 |
eq( |
||
| 408 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 409 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 410 |
L_14 |
||
| 411 |
); |
||
| 412 |
} |
||
| 413 | |||
| 414 |
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
||
| 415 |
t( |
||
| 416 |
!vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 417 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 418 |
L_15 |
||
| 419 |
); |
||
| 420 |
} |
||
| 421 | |||
| 422 |
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
||
| 423 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 424 |
} |
||
| 425 |
√
|
} else if (vars.sortedCdpsSizeBefore > _n) {
|
|
| 426 |
√
|
if (_atLeastOneCdpIsLiquidatable(cdpsBefore, vars.isRecoveryModeBefore)) {
|
|
| 427 |
assertRevertReasonNotEqual(returnData, "Panic(17)"); |
||
| 428 |
} |
||
| 429 |
} |
||
| 430 |
} |
||
| 431 | |||
| 432 |
function redeemCollateral( |
||
| 433 |
uint _EBTCAmount, |
||
| 434 |
uint _partialRedemptionHintNICR, |
||
| 435 |
uint _maxFeePercentage, |
||
| 436 |
uint _maxIterations |
||
| 437 |
) public setup {
|
||
| 438 |
⟳
|
bool success; |
|
| 439 |
⟳
|
bytes memory returnData; |
|
| 440 | |||
| 441 |
⟳
|
_EBTCAmount = between(_EBTCAmount, 0, eBTCToken.balanceOf(address(actor))); |
|
| 442 |
⟳
|
_maxIterations = between(_maxIterations, 0, 1); |
|
| 443 | |||
| 444 |
⟳
|
_maxFeePercentage = between( |
|
| 445 |
⟳
|
_maxFeePercentage, |
|
| 446 |
⟳
|
cdpManager.redemptionFeeFloor(), |
|
| 447 |
⟳
|
cdpManager.DECIMAL_PRECISION() |
|
| 448 |
); |
||
| 449 | |||
| 450 |
⟳
|
bytes32 _cdpId = _getFirstCdpWithIcrGteMcr(); |
|
| 451 | |||
| 452 |
⟳
|
_before(_cdpId); |
|
| 453 | |||
| 454 |
⟳
|
(success, returnData) = actor.proxy( |
|
| 455 |
⟳
|
address(cdpManager), |
|
| 456 |
abi.encodeWithSelector( |
||
| 457 |
⟳
|
CdpManager.redeemCollateral.selector, |
|
| 458 |
⟳
|
_EBTCAmount, |
|
| 459 |
⟳
|
bytes32(0), |
|
| 460 |
⟳
|
bytes32(0), |
|
| 461 |
⟳
|
bytes32(0), |
|
| 462 |
⟳
|
_partialRedemptionHintNICR, |
|
| 463 |
⟳
|
_maxIterations, |
|
| 464 |
⟳
|
_maxFeePercentage |
|
| 465 |
) |
||
| 466 |
); |
||
| 467 | |||
| 468 |
⟳
|
require(success); |
|
| 469 | |||
| 470 |
⟳
|
_after(_cdpId); |
|
| 471 | |||
| 472 |
⟳
|
gt(vars.tcrBefore, cdpManager.MCR(), EBTC_02); |
|
| 473 |
⟳
|
if (_maxIterations == 1) {
|
|
| 474 |
⟳
|
gte(vars.activePoolDebtBefore, vars.activePoolDebtAfter, CDPM_05); |
|
| 475 |
⟳
|
gte(vars.cdpDebtBefore, vars.cdpDebtAfter, CDPM_06); |
|
| 476 |
// TODO: CHECK THIS |
||
| 477 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/10#issuecomment-1702685732 |
||
| 478 |
⟳
|
if (vars.sortedCdpsSizeBefore == vars.sortedCdpsSizeAfter) {
|
|
| 479 |
// Redemptions do not reduce TCR |
||
| 480 |
// If redemptions do not close any CDP that was healthy (low debt, high coll) |
||
| 481 |
gt( |
||
| 482 |
vars.newTcrAfter, // TODO: See how this breaks |
||
| 483 |
vars.newTcrBefore, |
||
| 484 |
R_07 |
||
| 485 |
); |
||
| 486 |
} |
||
| 487 |
⟳
|
t(invariant_CDPM_04(vars), CDPM_04); |
|
| 488 |
} |
||
| 489 |
⟳
|
gt(vars.actorEbtcBefore, vars.actorEbtcAfter, R_08); |
|
| 490 | |||
| 491 |
// Verify Fee Recipient Received the Fee |
||
| 492 |
⟳
|
gte(vars.feeRecipientTotalCollAfter, vars.feeRecipientTotalCollBefore, F_02); |
|
| 493 | |||
| 494 |
if ( |
||
| 495 |
⟳
|
vars.lastGracePeriodStartTimestampIsSetBefore && |
|
| 496 |
⟳
|
vars.isRecoveryModeBefore && |
|
| 497 |
vars.isRecoveryModeAfter |
||
| 498 |
) {
|
||
| 499 |
eq( |
||
| 500 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 501 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 502 |
L_14 |
||
| 503 |
); |
||
| 504 |
} |
||
| 505 | |||
| 506 |
⟳
|
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
|
| 507 |
⟳
|
t( |
|
| 508 |
⟳
|
!vars.lastGracePeriodStartTimestampIsSetBefore && |
|
| 509 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 510 |
⟳
|
L_15 |
|
| 511 |
); |
||
| 512 |
} |
||
| 513 | |||
| 514 |
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
||
| 515 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 516 |
} |
||
| 517 |
} |
||
| 518 | |||
| 519 |
/////////////////////////////////////////////////////// |
||
| 520 |
// ActivePool |
||
| 521 |
/////////////////////////////////////////////////////// |
||
| 522 | |||
| 523 |
function flashLoanColl(uint _amount) internal setup {
|
||
| 524 |
bool success; |
||
| 525 |
bytes memory returnData; |
||
| 526 | |||
| 527 |
_amount = between(_amount, 0, activePool.maxFlashLoan(address(collateral))); |
||
| 528 |
uint _fee = activePool.flashFee(address(collateral), _amount); |
||
| 529 | |||
| 530 |
_before(bytes32(0)); |
||
| 531 | |||
| 532 |
// take the flashloan which should always cost the fee paid by caller |
||
| 533 |
uint _balBefore = collateral.balanceOf(activePool.feeRecipientAddress()); |
||
| 534 |
(success, returnData) = actor.proxy( |
||
| 535 |
address(activePool), |
||
| 536 |
abi.encodeWithSelector( |
||
| 537 |
ActivePool.flashLoan.selector, |
||
| 538 |
IERC3156FlashBorrower(address(actor)), |
||
| 539 |
address(collateral), |
||
| 540 |
_amount, |
||
| 541 |
_getFlashLoanActions(_amount) |
||
| 542 |
) |
||
| 543 |
); |
||
| 544 | |||
| 545 |
require(success); |
||
| 546 | |||
| 547 |
_after(bytes32(0)); |
||
| 548 | |||
| 549 |
uint _balAfter = collateral.balanceOf(activePool.feeRecipientAddress()); |
||
| 550 |
eq(_balAfter - _balBefore, _fee, F_03); |
||
| 551 | |||
| 552 |
if ( |
||
| 553 |
vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 554 |
vars.isRecoveryModeBefore && |
||
| 555 |
vars.isRecoveryModeAfter |
||
| 556 |
) {
|
||
| 557 |
eq( |
||
| 558 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 559 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 560 |
L_14 |
||
| 561 |
); |
||
| 562 |
} |
||
| 563 | |||
| 564 |
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
||
| 565 |
t( |
||
| 566 |
!vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 567 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 568 |
L_15 |
||
| 569 |
); |
||
| 570 |
} |
||
| 571 | |||
| 572 |
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
||
| 573 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 574 |
} |
||
| 575 |
} |
||
| 576 | |||
| 577 |
/////////////////////////////////////////////////////// |
||
| 578 |
// BorrowerOperations |
||
| 579 |
/////////////////////////////////////////////////////// |
||
| 580 | |||
| 581 |
function flashLoanEBTC(uint _amount) internal setup {
|
||
| 582 |
bool success; |
||
| 583 |
bytes memory returnData; |
||
| 584 | |||
| 585 |
_amount = between(_amount, 0, borrowerOperations.maxFlashLoan(address(eBTCToken))); |
||
| 586 | |||
| 587 |
uint _fee = borrowerOperations.flashFee(address(eBTCToken), _amount); |
||
| 588 | |||
| 589 |
_before(bytes32(0)); |
||
| 590 | |||
| 591 |
// take the flashloan which should always cost the fee paid by caller |
||
| 592 |
uint _balBefore = eBTCToken.balanceOf(borrowerOperations.feeRecipientAddress()); |
||
| 593 |
(success, returnData) = actor.proxy( |
||
| 594 |
address(borrowerOperations), |
||
| 595 |
abi.encodeWithSelector( |
||
| 596 |
BorrowerOperations.flashLoan.selector, |
||
| 597 |
IERC3156FlashBorrower(address(actor)), |
||
| 598 |
address(eBTCToken), |
||
| 599 |
_amount, |
||
| 600 |
_getFlashLoanActions(_amount) |
||
| 601 |
) |
||
| 602 |
); |
||
| 603 | |||
| 604 |
// BorrowerOperations.flashLoan may revert due to reentrancy |
||
| 605 |
require(success); |
||
| 606 | |||
| 607 |
_after(bytes32(0)); |
||
| 608 | |||
| 609 |
uint _balAfter = eBTCToken.balanceOf(borrowerOperations.feeRecipientAddress()); |
||
| 610 |
eq(_balAfter - _balBefore, _fee, F_03); |
||
| 611 | |||
| 612 |
if ( |
||
| 613 |
vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 614 |
vars.isRecoveryModeBefore && |
||
| 615 |
vars.isRecoveryModeAfter |
||
| 616 |
) {
|
||
| 617 |
eq( |
||
| 618 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 619 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 620 |
L_14 |
||
| 621 |
); |
||
| 622 |
} |
||
| 623 | |||
| 624 |
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
||
| 625 |
t( |
||
| 626 |
!vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 627 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 628 |
L_15 |
||
| 629 |
); |
||
| 630 |
} |
||
| 631 | |||
| 632 |
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
||
| 633 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 634 |
} |
||
| 635 |
} |
||
| 636 | |||
| 637 |
√
|
⟳
|
function openCdp(uint256 _col, uint256 _EBTCAmount) public setup returns (bytes32 _cdpId) {
|
| 638 |
√
|
⟳
|
bool success; |
| 639 |
√
|
⟳
|
bytes memory returnData; |
| 640 | |||
| 641 |
// we pass in CCR instead of MCR in case it's the first one |
||
| 642 |
√
|
⟳
|
uint price = priceFeedMock.getPrice(); |
| 643 | |||
| 644 |
√
|
⟳
|
uint256 requiredCollAmount = (_EBTCAmount * cdpManager.CCR()) / (price); |
| 645 |
√
|
⟳
|
uint256 minCollAmount = max( |
| 646 |
√
|
⟳
|
cdpManager.MIN_NET_COLL() + borrowerOperations.LIQUIDATOR_REWARD(), |
| 647 |
√
|
⟳
|
requiredCollAmount |
| 648 |
); |
||
| 649 |
√
|
⟳
|
uint256 maxCollAmount = min(2 * minCollAmount, INITIAL_COLL_BALANCE / 10); |
| 650 |
√
|
⟳
|
_col = between(requiredCollAmount, minCollAmount, maxCollAmount); |
| 651 | |||
| 652 |
√
|
⟳
|
(success, ) = actor.proxy( |
| 653 |
√
|
⟳
|
address(collateral), |
| 654 |
abi.encodeWithSelector( |
||
| 655 |
√
|
⟳
|
CollateralTokenTester.approve.selector, |
| 656 |
√
|
⟳
|
address(borrowerOperations), |
| 657 |
√
|
⟳
|
_col |
| 658 |
) |
||
| 659 |
); |
||
| 660 |
√
|
⟳
|
t(success, "Approve never fails"); |
| 661 | |||
| 662 |
√
|
⟳
|
_before(bytes32(0)); |
| 663 | |||
| 664 |
√
|
(success, returnData) = actor.proxy( |
|
| 665 |
√
|
address(borrowerOperations), |
|
| 666 |
abi.encodeWithSelector( |
||
| 667 |
√
|
BorrowerOperations.openCdp.selector, |
|
| 668 |
√
|
_EBTCAmount, |
|
| 669 |
√
|
bytes32(0), |
|
| 670 |
√
|
bytes32(0), |
|
| 671 |
√
|
_col |
|
| 672 |
) |
||
| 673 |
); |
||
| 674 | |||
| 675 |
√
|
if (success) {
|
|
| 676 |
√
|
_cdpId = abi.decode(returnData, (bytes32)); |
|
| 677 |
√
|
_after(_cdpId); |
|
| 678 | |||
| 679 |
√
|
t(invariant_GENERAL_01(vars), GENERAL_01); |
|
| 680 |
√
|
gt(vars.icrAfter, cdpManager.MCR(), BO_01); |
|
| 681 | |||
| 682 |
√
|
eq(vars.newTcrAfter, vars.tcrAfter, GENERAL_11); |
|
| 683 | |||
| 684 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/3 |
||
| 685 |
√
|
t(invariant_GENERAL_09(cdpManager, vars), GENERAL_09); |
|
| 686 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/4 |
||
| 687 |
√
|
gte( |
|
| 688 |
√
|
collateral.getPooledEthByShares(cdpManager.getCdpCollShares(_cdpId)), |
|
| 689 |
√
|
borrowerOperations.MIN_NET_COLL(), |
|
| 690 |
√
|
GENERAL_10 |
|
| 691 |
); |
||
| 692 |
√
|
eq( |
|
| 693 |
√
|
vars.sortedCdpsSizeBefore + 1, |
|
| 694 |
√
|
vars.sortedCdpsSizeAfter, |
|
| 695 |
"CDPs count must have increased" |
||
| 696 |
); |
||
| 697 |
if ( |
||
| 698 |
√
|
vars.lastGracePeriodStartTimestampIsSetBefore && |
|
| 699 |
vars.isRecoveryModeBefore && |
||
| 700 |
vars.isRecoveryModeAfter |
||
| 701 |
) {
|
||
| 702 |
eq( |
||
| 703 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 704 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 705 |
L_14 |
||
| 706 |
); |
||
| 707 |
} |
||
| 708 | |||
| 709 |
√
|
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
|
| 710 |
t( |
||
| 711 |
!vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 712 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 713 |
L_15 |
||
| 714 |
); |
||
| 715 |
} |
||
| 716 | |||
| 717 |
√
|
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
|
| 718 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 719 |
} |
||
| 720 |
} else {
|
||
| 721 |
√
|
assertRevertReasonNotEqual(returnData, "Panic(17)"); |
|
| 722 |
} |
||
| 723 |
} |
||
| 724 | |||
| 725 |
function addColl(uint _coll, uint256 _i) public setup {
|
||
| 726 |
√
|
⟳
|
bool success; |
| 727 |
√
|
⟳
|
bytes memory returnData; |
| 728 | |||
| 729 |
√
|
⟳
|
uint256 numberOfCdps = sortedCdps.cdpCountOf(address(actor)); |
| 730 |
√
|
⟳
|
require(numberOfCdps > 0, "Actor must have at least one CDP open"); |
| 731 | |||
| 732 |
√
|
_i = between(_i, 0, numberOfCdps - 1); |
|
| 733 |
√
|
bytes32 _cdpId = sortedCdps.cdpOfOwnerByIndex(address(actor), _i); |
|
| 734 |
√
|
t(_cdpId != bytes32(0), "CDP ID must not be null if the index is valid"); |
|
| 735 | |||
| 736 |
√
|
_coll = between(_coll, 0, INITIAL_COLL_BALANCE / 10); |
|
| 737 | |||
| 738 |
√
|
if (collateral.balanceOf(address(actor)) < _coll) {
|
|
| 739 |
(success, ) = actor.proxy( |
||
| 740 |
address(collateral), |
||
| 741 |
abi.encodeWithSelector(CollateralTokenTester.deposit.selector, ""), |
||
| 742 |
(_coll - collateral.balanceOf(address(actor))) |
||
| 743 |
); |
||
| 744 |
require(success); |
||
| 745 |
require( |
||
| 746 |
collateral.balanceOf(address(actor)) > _coll, |
||
| 747 |
"Actor has high enough balance to add" |
||
| 748 |
); |
||
| 749 |
} |
||
| 750 | |||
| 751 |
√
|
(success, ) = actor.proxy( |
|
| 752 |
√
|
address(collateral), |
|
| 753 |
abi.encodeWithSelector( |
||
| 754 |
√
|
CollateralTokenTester.approve.selector, |
|
| 755 |
√
|
address(borrowerOperations), |
|
| 756 |
√
|
_coll |
|
| 757 |
) |
||
| 758 |
); |
||
| 759 |
√
|
t(success, "Approve never fails"); |
|
| 760 | |||
| 761 |
√
|
_before(_cdpId); |
|
| 762 | |||
| 763 |
√
|
(success, returnData) = actor.proxy( |
|
| 764 |
√
|
address(borrowerOperations), |
|
| 765 |
abi.encodeWithSelector( |
||
| 766 |
√
|
BorrowerOperations.addColl.selector, |
|
| 767 |
√
|
_cdpId, |
|
| 768 |
√
|
_cdpId, |
|
| 769 |
√
|
_cdpId, |
|
| 770 |
√
|
_coll |
|
| 771 |
) |
||
| 772 |
); |
||
| 773 | |||
| 774 |
√
|
_after(_cdpId); |
|
| 775 | |||
| 776 |
√
|
if (success) {
|
|
| 777 |
emit L3( |
||
| 778 |
√
|
vars.isRecoveryModeBefore ? 1 : 0, |
|
| 779 |
√
|
vars.hasGracePeriodPassedBefore ? 1 : 0, |
|
| 780 |
√
|
vars.icrAfter |
|
| 781 |
); |
||
| 782 |
emit L3( |
||
| 783 |
√
|
block.timestamp, |
|
| 784 |
√
|
cdpManager.lastGracePeriodStartTimestamp(), |
|
| 785 |
√
|
cdpManager.recoveryModeGracePeriodDuration() |
|
| 786 |
); |
||
| 787 | |||
| 788 |
√
|
eq(vars.newTcrAfter, vars.tcrAfter, GENERAL_11); |
|
| 789 |
√
|
gte(vars.nicrAfter, vars.nicrBefore, BO_03); |
|
| 790 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/3 |
||
| 791 |
√
|
t(invariant_GENERAL_09(cdpManager, vars), GENERAL_09); |
|
| 792 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/4 |
||
| 793 |
√
|
gte( |
|
| 794 |
√
|
collateral.getPooledEthByShares(cdpManager.getCdpCollShares(_cdpId)), |
|
| 795 |
√
|
borrowerOperations.MIN_NET_COLL(), |
|
| 796 |
√
|
GENERAL_10 |
|
| 797 |
); |
||
| 798 | |||
| 799 |
√
|
t(invariant_GENERAL_01(vars), GENERAL_01); |
|
| 800 | |||
| 801 |
if ( |
||
| 802 |
√
|
vars.lastGracePeriodStartTimestampIsSetBefore && |
|
| 803 |
vars.isRecoveryModeBefore && |
||
| 804 |
vars.isRecoveryModeAfter |
||
| 805 |
) {
|
||
| 806 |
eq( |
||
| 807 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 808 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 809 |
L_14 |
||
| 810 |
); |
||
| 811 |
} |
||
| 812 | |||
| 813 |
√
|
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
|
| 814 |
t( |
||
| 815 |
!vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 816 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 817 |
L_15 |
||
| 818 |
); |
||
| 819 |
} |
||
| 820 | |||
| 821 |
√
|
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
|
| 822 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 823 |
} |
||
| 824 |
} else {
|
||
| 825 |
assertRevertReasonNotEqual(returnData, "Panic(17)"); |
||
| 826 |
} |
||
| 827 |
} |
||
| 828 | |||
| 829 |
function withdrawColl(uint _amount, uint256 _i) public setup {
|
||
| 830 |
√
|
⟳
|
bool success; |
| 831 |
√
|
⟳
|
bytes memory returnData; |
| 832 | |||
| 833 |
√
|
⟳
|
uint256 numberOfCdps = sortedCdps.cdpCountOf(address(actor)); |
| 834 |
√
|
⟳
|
require(numberOfCdps > 0, "Actor must have at least one CDP open"); |
| 835 | |||
| 836 |
√
|
_i = between(_i, 0, numberOfCdps - 1); |
|
| 837 |
√
|
bytes32 _cdpId = sortedCdps.cdpOfOwnerByIndex(address(actor), _i); |
|
| 838 |
√
|
t(_cdpId != bytes32(0), "CDP ID must not be null if the index is valid"); |
|
| 839 | |||
| 840 |
// Can only withdraw up to CDP collateral amount, otherwise will revert with assert |
||
| 841 |
√
|
_amount = between( |
|
| 842 |
√
|
_amount, |
|
| 843 |
√
|
0, |
|
| 844 |
√
|
collateral.getPooledEthByShares(cdpManager.getCdpCollShares(_cdpId)) |
|
| 845 |
); |
||
| 846 | |||
| 847 |
√
|
_before(_cdpId); |
|
| 848 | |||
| 849 |
√
|
(success, returnData) = actor.proxy( |
|
| 850 |
√
|
address(borrowerOperations), |
|
| 851 |
abi.encodeWithSelector( |
||
| 852 |
√
|
BorrowerOperations.withdrawColl.selector, |
|
| 853 |
√
|
_cdpId, |
|
| 854 |
√
|
_amount, |
|
| 855 |
√
|
_cdpId, |
|
| 856 |
√
|
_cdpId |
|
| 857 |
) |
||
| 858 |
); |
||
| 859 | |||
| 860 |
√
|
_after(_cdpId); |
|
| 861 | |||
| 862 |
√
|
if (success) {
|
|
| 863 |
√
|
eq(vars.newTcrAfter, vars.tcrAfter, GENERAL_11); |
|
| 864 |
√
|
lte(vars.nicrAfter, vars.nicrBefore, BO_04); |
|
| 865 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/3 |
||
| 866 |
√
|
t(invariant_GENERAL_09(cdpManager, vars), GENERAL_09); |
|
| 867 |
√
|
t(invariant_GENERAL_01(vars), GENERAL_01); |
|
| 868 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/4 |
||
| 869 |
√
|
gte( |
|
| 870 |
√
|
collateral.getPooledEthByShares(cdpManager.getCdpCollShares(_cdpId)), |
|
| 871 |
√
|
borrowerOperations.MIN_NET_COLL(), |
|
| 872 |
√
|
GENERAL_10 |
|
| 873 |
); |
||
| 874 | |||
| 875 |
if ( |
||
| 876 |
√
|
vars.lastGracePeriodStartTimestampIsSetBefore && |
|
| 877 |
vars.isRecoveryModeBefore && |
||
| 878 |
vars.isRecoveryModeAfter |
||
| 879 |
) {
|
||
| 880 |
eq( |
||
| 881 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 882 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 883 |
L_14 |
||
| 884 |
); |
||
| 885 |
} |
||
| 886 | |||
| 887 |
√
|
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
|
| 888 |
t( |
||
| 889 |
!vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 890 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 891 |
L_15 |
||
| 892 |
); |
||
| 893 |
} |
||
| 894 | |||
| 895 |
√
|
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
|
| 896 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 897 |
} |
||
| 898 |
} else {
|
||
| 899 |
√
|
assertRevertReasonNotEqual(returnData, "Panic(17)"); |
|
| 900 |
} |
||
| 901 |
} |
||
| 902 | |||
| 903 |
function withdrawDebt(uint _amount, uint256 _i) public setup {
|
||
| 904 |
√
|
⟳
|
bool success; |
| 905 |
√
|
⟳
|
bytes memory returnData; |
| 906 | |||
| 907 |
√
|
⟳
|
uint256 numberOfCdps = sortedCdps.cdpCountOf(address(actor)); |
| 908 |
√
|
⟳
|
require(numberOfCdps > 0, "Actor must have at least one CDP open"); |
| 909 | |||
| 910 |
√
|
⟳
|
_i = between(_i, 0, numberOfCdps - 1); |
| 911 |
√
|
⟳
|
bytes32 _cdpId = sortedCdps.cdpOfOwnerByIndex(address(actor), _i); |
| 912 |
√
|
⟳
|
t(_cdpId != bytes32(0), "CDP ID must not be null if the index is valid"); |
| 913 | |||
| 914 |
// TODO verify the assumption below, maybe there's a more sensible (or Governance-defined/hardcoded) limit for the maximum amount of minted eBTC at a single operation |
||
| 915 |
// Can only withdraw up to type(uint128).max eBTC, so that `BorrwerOperations._getNewCdpAmounts` does not overflow |
||
| 916 |
√
|
⟳
|
_amount = between(_amount, 0, type(uint128).max); |
| 917 | |||
| 918 |
√
|
⟳
|
_before(_cdpId); |
| 919 | |||
| 920 |
√
|
⟳
|
(success, returnData) = actor.proxy( |
| 921 |
√
|
⟳
|
address(borrowerOperations), |
| 922 |
abi.encodeWithSelector( |
||
| 923 |
√
|
⟳
|
BorrowerOperations.withdrawDebt.selector, |
| 924 |
√
|
⟳
|
_cdpId, |
| 925 |
√
|
⟳
|
_amount, |
| 926 |
√
|
⟳
|
_cdpId, |
| 927 |
√
|
⟳
|
_cdpId |
| 928 |
) |
||
| 929 |
); |
||
| 930 | |||
| 931 |
√
|
⟳
|
require(success); |
| 932 | |||
| 933 |
√
|
_after(_cdpId); |
|
| 934 | |||
| 935 |
√
|
eq(vars.newTcrAfter, vars.tcrAfter, GENERAL_11); |
|
| 936 |
√
|
gte(vars.cdpDebtAfter, vars.cdpDebtBefore, "withdrawDebt must not decrease debt"); |
|
| 937 |
√
|
eq( |
|
| 938 |
√
|
vars.actorEbtcAfter, |
|
| 939 |
√
|
vars.actorEbtcBefore + _amount, |
|
| 940 |
"withdrawDebt must increase debt by requested amount" |
||
| 941 |
); |
||
| 942 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/4 |
||
| 943 |
√
|
gte( |
|
| 944 |
√
|
collateral.getPooledEthByShares(cdpManager.getCdpCollShares(_cdpId)), |
|
| 945 |
√
|
borrowerOperations.MIN_NET_COLL(), |
|
| 946 |
√
|
GENERAL_10 |
|
| 947 |
); |
||
| 948 | |||
| 949 |
if ( |
||
| 950 |
√
|
vars.lastGracePeriodStartTimestampIsSetBefore && |
|
| 951 |
vars.isRecoveryModeBefore && |
||
| 952 |
vars.isRecoveryModeAfter |
||
| 953 |
) {
|
||
| 954 |
eq( |
||
| 955 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 956 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 957 |
L_14 |
||
| 958 |
); |
||
| 959 |
} |
||
| 960 | |||
| 961 |
√
|
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
|
| 962 |
t( |
||
| 963 |
!vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 964 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 965 |
L_15 |
||
| 966 |
); |
||
| 967 |
} |
||
| 968 | |||
| 969 |
√
|
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
|
| 970 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 971 |
} |
||
| 972 |
} |
||
| 973 | |||
| 974 |
function repayDebt(uint _amount, uint256 _i) public setup {
|
||
| 975 |
√
|
⟳
|
bool success; |
| 976 |
√
|
⟳
|
bytes memory returnData; |
| 977 | |||
| 978 |
√
|
⟳
|
uint256 numberOfCdps = sortedCdps.cdpCountOf(address(actor)); |
| 979 |
√
|
⟳
|
require(numberOfCdps > 0, "Actor must have at least one CDP open"); |
| 980 | |||
| 981 |
√
|
⟳
|
_i = between(_i, 0, numberOfCdps - 1); |
| 982 |
√
|
⟳
|
bytes32 _cdpId = sortedCdps.cdpOfOwnerByIndex(address(actor), _i); |
| 983 |
√
|
⟳
|
t(_cdpId != bytes32(0), "CDP ID must not be null if the index is valid"); |
| 984 | |||
| 985 |
√
|
⟳
|
(uint256 entireDebt, , ) = cdpManager.getDebtAndCollShares(_cdpId); |
| 986 |
√
|
⟳
|
_amount = between(_amount, 0, entireDebt); |
| 987 | |||
| 988 |
√
|
⟳
|
_before(_cdpId); |
| 989 | |||
| 990 |
√
|
⟳
|
(success, returnData) = actor.proxy( |
| 991 |
√
|
⟳
|
address(borrowerOperations), |
| 992 |
abi.encodeWithSelector( |
||
| 993 |
√
|
⟳
|
BorrowerOperations.repayDebt.selector, |
| 994 |
√
|
⟳
|
_cdpId, |
| 995 |
√
|
⟳
|
_amount, |
| 996 |
√
|
⟳
|
_cdpId, |
| 997 |
√
|
⟳
|
_cdpId |
| 998 |
) |
||
| 999 |
); |
||
| 1000 |
√
|
⟳
|
require(success); |
| 1001 | |||
| 1002 |
√
|
_after(_cdpId); |
|
| 1003 | |||
| 1004 |
√
|
eq(vars.newTcrAfter, vars.tcrAfter, GENERAL_11); |
|
| 1005 | |||
| 1006 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/3 |
||
| 1007 |
√
|
gte(vars.newTcrAfter, vars.newTcrBefore, BO_08); |
|
| 1008 | |||
| 1009 |
√
|
eq(vars.ebtcTotalSupplyBefore - _amount, vars.ebtcTotalSupplyAfter, BO_07); |
|
| 1010 |
√
|
eq(vars.actorEbtcBefore - _amount, vars.actorEbtcAfter, BO_07); |
|
| 1011 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/3 |
||
| 1012 |
√
|
t(invariant_GENERAL_09(cdpManager, vars), GENERAL_09); |
|
| 1013 |
√
|
t(invariant_GENERAL_01(vars), GENERAL_01); |
|
| 1014 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/4 |
||
| 1015 |
√
|
gte( |
|
| 1016 |
√
|
collateral.getPooledEthByShares(cdpManager.getCdpCollShares(_cdpId)), |
|
| 1017 |
√
|
borrowerOperations.MIN_NET_COLL(), |
|
| 1018 |
√
|
GENERAL_10 |
|
| 1019 |
); |
||
| 1020 | |||
| 1021 |
if ( |
||
| 1022 |
√
|
vars.lastGracePeriodStartTimestampIsSetBefore && |
|
| 1023 |
vars.isRecoveryModeBefore && |
||
| 1024 |
vars.isRecoveryModeAfter |
||
| 1025 |
) {
|
||
| 1026 |
eq( |
||
| 1027 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 1028 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 1029 |
L_14 |
||
| 1030 |
); |
||
| 1031 |
} |
||
| 1032 | |||
| 1033 |
√
|
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
|
| 1034 |
t( |
||
| 1035 |
!vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 1036 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 1037 |
L_15 |
||
| 1038 |
); |
||
| 1039 |
} |
||
| 1040 | |||
| 1041 |
√
|
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
|
| 1042 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 1043 |
} |
||
| 1044 |
} |
||
| 1045 | |||
| 1046 |
function closeCdp(uint _i) public setup {
|
||
| 1047 |
⟳
|
bool success; |
|
| 1048 |
⟳
|
bytes memory returnData; |
|
| 1049 | |||
| 1050 |
⟳
|
require(cdpManager.getActiveCdpsCount() > 1, "Cannot close last CDP"); |
|
| 1051 | |||
| 1052 |
⟳
|
uint256 numberOfCdps = sortedCdps.cdpCountOf(address(actor)); |
|
| 1053 |
⟳
|
require(numberOfCdps > 0, "Actor must have at least one CDP open"); |
|
| 1054 | |||
| 1055 |
_i = between(_i, 0, numberOfCdps - 1); |
||
| 1056 |
bytes32 _cdpId = sortedCdps.cdpOfOwnerByIndex(address(actor), _i); |
||
| 1057 |
t(_cdpId != bytes32(0), "CDP ID must not be null if the index is valid"); |
||
| 1058 | |||
| 1059 |
_before(_cdpId); |
||
| 1060 | |||
| 1061 |
(success, returnData) = actor.proxy( |
||
| 1062 |
address(borrowerOperations), |
||
| 1063 |
abi.encodeWithSelector(BorrowerOperations.closeCdp.selector, _cdpId) |
||
| 1064 |
); |
||
| 1065 | |||
| 1066 |
_after(_cdpId); |
||
| 1067 | |||
| 1068 |
if (success) {
|
||
| 1069 |
eq(vars.newTcrAfter, vars.tcrAfter, GENERAL_11); |
||
| 1070 |
eq(vars.cdpDebtAfter, 0, BO_02); |
||
| 1071 |
eq( |
||
| 1072 |
vars.sortedCdpsSizeBefore - 1, |
||
| 1073 |
vars.sortedCdpsSizeAfter, |
||
| 1074 |
"closeCdp reduces list size by 1" |
||
| 1075 |
); |
||
| 1076 |
gt( |
||
| 1077 |
vars.actorCollAfter, |
||
| 1078 |
vars.actorCollBefore, |
||
| 1079 |
"closeCdp increases the collateral balance of the user" |
||
| 1080 |
); |
||
| 1081 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/3 |
||
| 1082 |
t(invariant_GENERAL_09(cdpManager, vars), GENERAL_09); |
||
| 1083 |
emit L4( |
||
| 1084 |
vars.actorCollBefore, |
||
| 1085 |
vars.cdpCollBefore, |
||
| 1086 |
vars.liquidatorRewardSharesBefore, |
||
| 1087 |
vars.actorCollAfter |
||
| 1088 |
); |
||
| 1089 |
gt( |
||
| 1090 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/11 |
||
| 1091 |
// Note: not checking for strict equality since split fee is difficult to calculate a-priori, so the CDP collateral value may not be sent back to the user in full |
||
| 1092 |
vars.actorCollAfter, |
||
| 1093 |
vars.actorCollBefore + |
||
| 1094 |
// ActivePool transfer SHARES not ETH directly |
||
| 1095 |
collateral.getPooledEthByShares(vars.liquidatorRewardSharesBefore), |
||
| 1096 |
BO_05 |
||
| 1097 |
); |
||
| 1098 |
t(invariant_GENERAL_01(vars), GENERAL_01); |
||
| 1099 | |||
| 1100 |
if ( |
||
| 1101 |
vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 1102 |
vars.isRecoveryModeBefore && |
||
| 1103 |
vars.isRecoveryModeAfter |
||
| 1104 |
) {
|
||
| 1105 |
eq( |
||
| 1106 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 1107 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 1108 |
L_14 |
||
| 1109 |
); |
||
| 1110 |
} |
||
| 1111 | |||
| 1112 |
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
||
| 1113 |
t( |
||
| 1114 |
!vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 1115 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 1116 |
L_15 |
||
| 1117 |
); |
||
| 1118 |
} |
||
| 1119 | |||
| 1120 |
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
||
| 1121 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 1122 |
} |
||
| 1123 |
} else {
|
||
| 1124 |
assertRevertReasonNotEqual(returnData, "Panic(17)"); |
||
| 1125 |
} |
||
| 1126 |
} |
||
| 1127 | |||
| 1128 |
function adjustCdp( |
||
| 1129 |
uint _i, |
||
| 1130 |
uint _collWithdrawal, |
||
| 1131 |
uint _EBTCChange, |
||
| 1132 |
bool _isDebtIncrease |
||
| 1133 |
) public setup {
|
||
| 1134 |
√
|
⟳
|
bool success; |
| 1135 |
√
|
⟳
|
bytes memory returnData; |
| 1136 | |||
| 1137 |
√
|
⟳
|
uint256 numberOfCdps = sortedCdps.cdpCountOf(address(actor)); |
| 1138 |
√
|
⟳
|
require(numberOfCdps > 0, "Actor must have at least one CDP open"); |
| 1139 | |||
| 1140 |
√
|
⟳
|
_i = between(_i, 0, numberOfCdps - 1); |
| 1141 |
√
|
⟳
|
bytes32 _cdpId = sortedCdps.cdpOfOwnerByIndex(address(actor), _i); |
| 1142 |
√
|
⟳
|
t(_cdpId != bytes32(0), "CDP ID must not be null if the index is valid"); |
| 1143 | |||
| 1144 |
√
|
⟳
|
(uint256 entireDebt, uint256 entireColl, ) = cdpManager.getDebtAndCollShares(_cdpId); |
| 1145 |
√
|
⟳
|
_collWithdrawal = between(_collWithdrawal, 0, entireColl); |
| 1146 |
√
|
⟳
|
_EBTCChange = between(_EBTCChange, 0, entireDebt); |
| 1147 | |||
| 1148 |
√
|
⟳
|
_before(_cdpId); |
| 1149 | |||
| 1150 |
√
|
⟳
|
(success, returnData) = actor.proxy( |
| 1151 |
√
|
⟳
|
address(borrowerOperations), |
| 1152 |
abi.encodeWithSelector( |
||
| 1153 |
√
|
⟳
|
BorrowerOperations.adjustCdp.selector, |
| 1154 |
√
|
⟳
|
_cdpId, |
| 1155 |
√
|
⟳
|
_collWithdrawal, |
| 1156 |
√
|
⟳
|
_EBTCChange, |
| 1157 |
√
|
⟳
|
_isDebtIncrease, |
| 1158 |
√
|
⟳
|
_cdpId, |
| 1159 |
√
|
⟳
|
_cdpId |
| 1160 |
) |
||
| 1161 |
); |
||
| 1162 | |||
| 1163 |
√
|
⟳
|
require(success); |
| 1164 | |||
| 1165 |
√
|
_after(_cdpId); |
|
| 1166 | |||
| 1167 |
√
|
eq(vars.newTcrAfter, vars.tcrAfter, GENERAL_11); |
|
| 1168 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/3 |
||
| 1169 |
√
|
t(invariant_GENERAL_09(cdpManager, vars), GENERAL_09); |
|
| 1170 | |||
| 1171 |
√
|
t(invariant_GENERAL_01(vars), GENERAL_01); |
|
| 1172 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/4 |
||
| 1173 |
√
|
gte( |
|
| 1174 |
√
|
collateral.getPooledEthByShares(cdpManager.getCdpCollShares(_cdpId)), |
|
| 1175 |
√
|
borrowerOperations.MIN_NET_COLL(), |
|
| 1176 |
√
|
GENERAL_10 |
|
| 1177 |
); |
||
| 1178 | |||
| 1179 |
if ( |
||
| 1180 |
√
|
vars.lastGracePeriodStartTimestampIsSetBefore && |
|
| 1181 |
vars.isRecoveryModeBefore && |
||
| 1182 |
vars.isRecoveryModeAfter |
||
| 1183 |
) {
|
||
| 1184 |
eq( |
||
| 1185 |
vars.lastGracePeriodStartTimestampBefore, |
||
| 1186 |
vars.lastGracePeriodStartTimestampAfter, |
||
| 1187 |
L_14 |
||
| 1188 |
); |
||
| 1189 |
} |
||
| 1190 | |||
| 1191 |
√
|
if (!vars.isRecoveryModeBefore && vars.isRecoveryModeAfter) {
|
|
| 1192 |
t( |
||
| 1193 |
!vars.lastGracePeriodStartTimestampIsSetBefore && |
||
| 1194 |
vars.lastGracePeriodStartTimestampIsSetAfter, |
||
| 1195 |
L_15 |
||
| 1196 |
); |
||
| 1197 |
} |
||
| 1198 | |||
| 1199 |
√
|
if (vars.isRecoveryModeBefore && !vars.isRecoveryModeAfter) {
|
|
| 1200 |
t(!vars.lastGracePeriodStartTimestampIsSetAfter, L_16); |
||
| 1201 |
} |
||
| 1202 |
} |
||
| 1203 | |||
| 1204 |
/////////////////////////////////////////////////////// |
||
| 1205 |
// Collateral Token (Test) |
||
| 1206 |
/////////////////////////////////////////////////////// |
||
| 1207 | |||
| 1208 |
// Example for real world slashing: https://twitter.com/LidoFinance/status/1646505631678107649 |
||
| 1209 |
// > There are 11 slashing ongoing with the RockLogic GmbH node operator in Lido. |
||
| 1210 |
// > the total projected impact is around 20 ETH, |
||
| 1211 |
// > or about 3% of average daily protocol rewards/0.0004% of TVL. |
||
| 1212 |
function setEthPerShare(uint256 _newEthPerShare) public {
|
||
| 1213 |
√
|
uint256 currentEthPerShare = collateral.getEthPerShare(); |
|
| 1214 |
√
|
_newEthPerShare = between( |
|
| 1215 |
√
|
_newEthPerShare, |
|
| 1216 |
√
|
(currentEthPerShare * 1e18) / MAX_REBASE_PERCENT, |
|
| 1217 |
√
|
(currentEthPerShare * MAX_REBASE_PERCENT) / 1e18 |
|
| 1218 |
); |
||
| 1219 |
√
|
collateral.setEthPerShare(_newEthPerShare); |
|
| 1220 |
} |
||
| 1221 | |||
| 1222 |
/////////////////////////////////////////////////////// |
||
| 1223 |
// PriceFeed |
||
| 1224 |
/////////////////////////////////////////////////////// |
||
| 1225 | |||
| 1226 |
function setPrice(uint256 _newPrice) public {
|
||
| 1227 |
√
|
uint256 currentPrice = priceFeedMock.getPrice(); |
|
| 1228 |
√
|
_newPrice = between( |
|
| 1229 |
√
|
_newPrice, |
|
| 1230 |
√
|
(currentPrice * 1e18) / MAX_PRICE_CHANGE_PERCENT, |
|
| 1231 |
√
|
(currentPrice * MAX_PRICE_CHANGE_PERCENT) / 1e18 |
|
| 1232 |
); |
||
| 1233 |
√
|
priceFeedMock.setPrice(_newPrice); |
|
| 1234 |
} |
||
| 1235 | |||
| 1236 |
/////////////////////////////////////////////////////// |
||
| 1237 |
// Governance |
||
| 1238 |
/////////////////////////////////////////////////////// |
||
| 1239 | |||
| 1240 |
function setGovernanceParameters(uint256 parameter, uint256 value) public {
|
||
| 1241 |
√
|
⟳
|
parameter = between(parameter, 0, 6); |
| 1242 | |||
| 1243 |
√
|
⟳
|
if (parameter == 0) {
|
| 1244 |
√
|
value = between(value, cdpManager.MINIMUM_GRACE_PERIOD(), type(uint128).max); |
|
| 1245 |
√
|
hevm.prank(defaultGovernance); |
|
| 1246 |
√
|
cdpManager.setGracePeriod(uint128(value)); |
|
| 1247 |
√
|
⟳
|
} else if (parameter == 1) {
|
| 1248 |
√
|
⟳
|
value = between(value, 0, activePool.getFeeRecipientClaimableCollShares()); |
| 1249 |
√
|
⟳
|
_before(bytes32(0)); |
| 1250 |
√
|
⟳
|
hevm.prank(defaultGovernance); |
| 1251 |
√
|
⟳
|
activePool.claimFeeRecipientCollShares(value); |
| 1252 |
√
|
⟳
|
_after(bytes32(0)); |
| 1253 |
// If there was something to claim |
||
| 1254 |
√
|
⟳
|
if(value > 0) {
|
| 1255 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/22 |
||
| 1256 |
// Claiming will increase the balance |
||
| 1257 |
⟳
|
gte(vars.feeRecipientCollSharesAfter, vars.feeRecipientCollSharesBefore, F_01); |
|
| 1258 |
} |
||
| 1259 |
√
|
} else if (parameter == 2) {
|
|
| 1260 |
√
|
value = between(value, 0, cdpManager.MAX_REWARD_SPLIT()); |
|
| 1261 |
√
|
hevm.prank(defaultGovernance); |
|
| 1262 |
√
|
cdpManager.setStakingRewardSplit(value); |
|
| 1263 |
√
|
} else if (parameter == 3) {
|
|
| 1264 |
√
|
value = between( |
|
| 1265 |
√
|
value, |
|
| 1266 |
√
|
cdpManager.MIN_REDEMPTION_FEE_FLOOR(), |
|
| 1267 |
√
|
cdpManager.DECIMAL_PRECISION() |
|
| 1268 |
); |
||
| 1269 |
√
|
hevm.prank(defaultGovernance); |
|
| 1270 |
√
|
cdpManager.setRedemptionFeeFloor(value); |
|
| 1271 |
√
|
} else if (parameter == 4) {
|
|
| 1272 |
√
|
value = between( |
|
| 1273 |
√
|
value, |
|
| 1274 |
√
|
cdpManager.MIN_MINUTE_DECAY_FACTOR(), |
|
| 1275 |
√
|
cdpManager.MAX_MINUTE_DECAY_FACTOR() |
|
| 1276 |
); |
||
| 1277 |
√
|
hevm.prank(defaultGovernance); |
|
| 1278 |
√
|
cdpManager.setMinuteDecayFactor(value); |
|
| 1279 |
√
|
} else if (parameter == 5) {
|
|
| 1280 |
√
|
value = between(value, 0, cdpManager.DECIMAL_PRECISION()); |
|
| 1281 |
√
|
hevm.prank(defaultGovernance); |
|
| 1282 |
√
|
cdpManager.setBeta(value); |
|
| 1283 |
√
|
} else if (parameter == 6) {
|
|
| 1284 |
√
|
value = between(value, 0, 1); |
|
| 1285 |
√
|
hevm.prank(defaultGovernance); |
|
| 1286 |
√
|
cdpManager.setRedemptionsPaused(value == 1 ? true : false); |
|
| 1287 |
} |
||
| 1288 |
} |
||
| 1289 |
} |
||
| 1290 |
| Lines covered: | 8 / 8 (100.0%) |
|---|
| 1 |
pragma solidity 0.8.17; |
||
| 2 | |||
| 3 |
import "@crytic/properties/contracts/util/PropertiesHelper.sol"; |
||
| 4 |
import "../Asserts.sol"; |
||
| 5 | |||
| 6 |
abstract contract EchidnaAsserts is PropertiesAsserts, Asserts {
|
||
| 7 |
function gt(uint256 a, uint256 b, string memory message) internal override {
|
||
| 8 |
√
|
⟳
|
assertGt(a, b, message); |
| 9 |
} |
||
| 10 | |||
| 11 |
function lt(uint256 a, uint256 b, string memory message) internal override {
|
||
| 12 |
√
|
assertLt(a, b, message); |
|
| 13 |
} |
||
| 14 | |||
| 15 |
function gte(uint256 a, uint256 b, string memory message) internal override {
|
||
| 16 |
√
|
⟳
|
assertGte(a, b, message); |
| 17 |
} |
||
| 18 | |||
| 19 |
function lte(uint256 a, uint256 b, string memory message) internal override {
|
||
| 20 |
√
|
assertLte(a, b, message); |
|
| 21 |
} |
||
| 22 | |||
| 23 |
function eq(uint256 a, uint256 b, string memory message) internal override {
|
||
| 24 |
√
|
assertEq(a, b, message); |
|
| 25 |
} |
||
| 26 | |||
| 27 |
function t(bool a, string memory message) internal override {
|
||
| 28 |
√
|
⟳
|
assertWithMsg(a, message); |
| 29 |
} |
||
| 30 | |||
| 31 |
√
|
⟳
|
function between(uint256 value, uint256 low, uint256 high) internal override returns (uint256) {
|
| 32 |
√
|
⟳
|
return clampBetween(value, low, high); |
| 33 |
} |
||
| 34 |
} |
||
| 35 |
| Lines covered: | 56 / 56 (100.0%) |
|---|
| 1 |
pragma solidity 0.8.17; |
||
| 2 | |||
| 3 |
import {TargetContractSetup} from "../TargetContractSetup.sol";
|
||
| 4 |
import {Properties} from "../Properties.sol";
|
||
| 5 | |||
| 6 |
abstract contract EchidnaProperties is TargetContractSetup, Properties {
|
||
| 7 |
√
|
function echidna_price() public returns (bool) {
|
|
| 8 |
√
|
return invariant_DUMMY_01(priceFeedMock); |
|
| 9 |
} |
||
| 10 | |||
| 11 |
√
|
function echidna_active_pool_invariant_1() public returns (bool) {
|
|
| 12 |
√
|
return invariant_AP_01(collateral, activePool); |
|
| 13 |
} |
||
| 14 | |||
| 15 |
√
|
function echidna_active_pool_invariant_2() public returns (bool) {
|
|
| 16 |
√
|
return invariant_AP_02(cdpManager, activePool); |
|
| 17 |
} |
||
| 18 | |||
| 19 |
√
|
function echidna_active_pool_invariant_3() public returns (bool) {
|
|
| 20 |
√
|
return invariant_AP_03(eBTCToken, activePool); |
|
| 21 |
} |
||
| 22 | |||
| 23 |
√
|
function echidna_active_pool_invariant_4() public returns (bool) {
|
|
| 24 |
√
|
return invariant_AP_04(cdpManager, activePool, diff_tolerance); |
|
| 25 |
} |
||
| 26 | |||
| 27 |
√
|
function echidna_active_pool_invariant_5() public returns (bool) {
|
|
| 28 |
√
|
return invariant_AP_05(cdpManager, diff_tolerance); |
|
| 29 |
} |
||
| 30 | |||
| 31 |
√
|
function echidna_cdp_manager_invariant_1() public returns (bool) {
|
|
| 32 |
√
|
return invariant_CDPM_01(cdpManager, sortedCdps); |
|
| 33 |
} |
||
| 34 | |||
| 35 |
√
|
function echidna_cdp_manager_invariant_2() public returns (bool) {
|
|
| 36 |
√
|
return invariant_CDPM_02(cdpManager); |
|
| 37 |
} |
||
| 38 | |||
| 39 |
√
|
function echidna_cdp_manager_invariant_3() public returns (bool) {
|
|
| 40 |
√
|
return invariant_CDPM_03(cdpManager); |
|
| 41 |
} |
||
| 42 | |||
| 43 |
// CDPM_04 is a vars invariant |
||
| 44 | |||
| 45 |
√
|
function echidna_coll_surplus_pool_invariant_1() public returns (bool) {
|
|
| 46 |
√
|
return invariant_CSP_01(collateral, collSurplusPool); |
|
| 47 |
} |
||
| 48 | |||
| 49 |
√
|
function echidna_coll_surplus_pool_invariant_2() public returns (bool) {
|
|
| 50 |
√
|
return invariant_CSP_02(collSurplusPool); |
|
| 51 |
} |
||
| 52 | |||
| 53 |
√
|
function echidna_sorted_list_invariant_1() public returns (bool) {
|
|
| 54 |
√
|
return invariant_SL_01(cdpManager, sortedCdps); |
|
| 55 |
} |
||
| 56 | |||
| 57 |
√
|
function echidna_sorted_list_invariant_2() public returns (bool) {
|
|
| 58 |
√
|
return invariant_SL_02(cdpManager, sortedCdps, priceFeedMock); |
|
| 59 |
} |
||
| 60 | |||
| 61 |
√
|
function echidna_sorted_list_invariant_3() public returns (bool) {
|
|
| 62 |
√
|
return invariant_SL_03(cdpManager, priceFeedMock, sortedCdps); |
|
| 63 |
} |
||
| 64 | |||
| 65 |
// https://github.com/Badger-Finance/ebtc-fuzz-review/issues/15 |
||
| 66 |
√
|
function echidna_sorted_list_invariant_5() public returns (bool) {
|
|
| 67 |
√
|
return invariant_SL_05(crLens, sortedCdps); |
|
| 68 |
} |
||
| 69 | |||
| 70 |
// invariant_GENERAL_01 is a vars invariant |
||
| 71 | |||
| 72 |
√
|
function echidna_GENERAL_02() public returns (bool) {
|
|
| 73 |
√
|
return invariant_GENERAL_02(cdpManager, priceFeedMock, eBTCToken); |
|
| 74 |
} |
||
| 75 | |||
| 76 |
√
|
function echidna_GENERAL_03() public returns (bool) {
|
|
| 77 |
√
|
return invariant_GENERAL_03(cdpManager, borrowerOperations, eBTCToken, collateral); |
|
| 78 |
} |
||
| 79 | |||
| 80 |
√
|
function echidna_GENERAL_05() public returns (bool) {
|
|
| 81 |
√
|
return invariant_GENERAL_05(activePool, cdpManager, collateral); |
|
| 82 |
} |
||
| 83 | |||
| 84 |
√
|
function echidna_GENERAL_05_B() public returns (bool) {
|
|
| 85 |
√
|
return invariant_GENERAL_05_B(collSurplusPool, collateral); |
|
| 86 |
} |
||
| 87 | |||
| 88 |
√
|
function echidna_GENERAL_06() public returns (bool) {
|
|
| 89 |
√
|
return invariant_GENERAL_06(eBTCToken, cdpManager, sortedCdps); |
|
| 90 |
} |
||
| 91 | |||
| 92 |
√
|
function echidna_GENERAL_08() public returns (bool) {
|
|
| 93 |
√
|
return invariant_GENERAL_08(cdpManager, sortedCdps, priceFeedMock, collateral); |
|
| 94 |
} |
||
| 95 | |||
| 96 |
// invariant_GENERAL_09 is a vars |
||
| 97 | |||
| 98 |
√
|
function echidna_GENERAL_12() public returns (bool) {
|
|
| 99 |
√
|
return invariant_GENERAL_12(cdpManager, priceFeedMock, crLens); |
|
| 100 |
} |
||
| 101 | |||
| 102 |
√
|
function echidna_GENERAL_13() public returns (bool) {
|
|
| 103 |
√
|
return invariant_GENERAL_13(crLens, cdpManager, priceFeedMock, sortedCdps); |
|
| 104 |
} |
||
| 105 | |||
| 106 |
√
|
function echidna_GENERAL_14() public returns (bool) {
|
|
| 107 |
√
|
return invariant_GENERAL_14(crLens, cdpManager, sortedCdps); |
|
| 108 |
} |
||
| 109 | |||
| 110 |
√
|
function echidna_GENERAL_15() public returns (bool) {
|
|
| 111 |
√
|
return invariant_GENERAL_15(); |
|
| 112 |
} |
||
| 113 | |||
| 114 |
√
|
function echidna_LS_01() public returns (bool) {
|
|
| 115 |
return |
||
| 116 |
√
|
invariant_LS_01( |
|
| 117 |
√
|
cdpManager, |
|
| 118 |
√
|
liquidationSequencer, |
|
| 119 |
√
|
syncedLiquidationSequencer, |
|
| 120 |
√
|
priceFeedMock |
|
| 121 |
); |
||
| 122 |
} |
||
| 123 | |||
| 124 |
} |
||
| 125 |
| Lines covered: | 3 / 3 (100.0%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "./EchidnaAsserts.sol"; |
||
| 6 |
import "./EchidnaProperties.sol"; |
||
| 7 |
import "../TargetFunctions.sol"; |
||
| 8 | |||
| 9 |
√
|
⟳
|
contract EchidnaTester is EchidnaAsserts, EchidnaProperties, TargetFunctions {
|
| 10 |
constructor() payable {
|
||
| 11 |
√
|
_setUp(); |
|
| 12 |
√
|
_setUpActors(); |
|
| 13 |
} |
||
| 14 |
} |
||
| 15 |
| Lines covered: | 11 / 35 (31.4%) |
|---|
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 | |||
| 3 |
pragma solidity 0.8.17; |
||
| 4 | |||
| 5 |
import "../../Interfaces/IPriceFeed.sol"; |
||
| 6 |
import "../../Interfaces/IFallbackCaller.sol"; |
||
| 7 |
import "../../Dependencies/Ownable.sol"; |
||
| 8 |
import "../../Dependencies/AuthNoOwner.sol"; |
||
| 9 | |||
| 10 |
/* |
||
| 11 |
* PriceFeed placeholder for testnet and development. The price can be manually input or fetched from |
||
| 12 |
the Fallback's TestNet implementation. Backwards compatible with local test environment as it defaults to use |
||
| 13 |
the manual price. |
||
| 14 |
*/ |
||
| 15 |
contract PriceFeedTestnet is IPriceFeed, Ownable, AuthNoOwner {
|
||
| 16 |
// --- variables --- |
||
| 17 | |||
| 18 |
√
|
uint256 private _price = 7428 * 1e13; // stETH/BTC price == ~15.8118 ETH per BTC |
|
| 19 |
bool public _useFallback; |
||
| 20 |
IFallbackCaller public fallbackCaller; // Wrapper contract that calls the Fallback system |
||
| 21 | |||
| 22 |
constructor(address _authorityAddress) {
|
||
| 23 |
√
|
_initializeAuthority(_authorityAddress); |
|
| 24 |
} |
||
| 25 | |||
| 26 |
// --- Dependency setters --- |
||
| 27 | |||
| 28 |
function setAddresses( |
||
| 29 |
address _priceAggregatorAddress, // Not used but kept for compatibility with deployment script |
||
| 30 |
address _fallbackCallerAddress, |
||
| 31 |
address _authorityAddress |
||
| 32 |
) external onlyOwner {
|
||
| 33 |
fallbackCaller = IFallbackCaller(_fallbackCallerAddress); |
||
| 34 | |||
| 35 |
_initializeAuthority(_authorityAddress); |
||
| 36 | |||
| 37 |
renounceOwnership(); |
||
| 38 |
} |
||
| 39 | |||
| 40 |
// --- Functions --- |
||
| 41 | |||
| 42 |
// View price getter for simplicity in tests |
||
| 43 |
√
|
⟳
|
function getPrice() external view returns (uint256) {
|
| 44 |
√
|
⟳
|
return _price; |
| 45 |
} |
||
| 46 | |||
| 47 |
√
|
⟳
|
function fetchPrice() external override returns (uint256) {
|
| 48 |
// Fire an event just like the mainnet version would. |
||
| 49 |
// This lets the subgraph rely on events to get the latest price even when developing locally. |
||
| 50 |
√
|
⟳
|
if (_useFallback) {
|
| 51 |
FallbackResponse memory fallbackResponse = _getCurrentFallbackResponse(); |
||
| 52 |
if (fallbackResponse.success) {
|
||
| 53 |
_price = fallbackResponse.answer; |
||
| 54 |
} |
||
| 55 |
} |
||
| 56 |
√
|
⟳
|
emit LastGoodPriceUpdated(_price); |
| 57 |
√
|
⟳
|
return _price; |
| 58 |
} |
||
| 59 | |||
| 60 |
// Manual external price setter. |
||
| 61 |
√
|
function setPrice(uint256 price) external returns (bool) {
|
|
| 62 |
√
|
_price = price; |
|
| 63 |
√
|
return true; |
|
| 64 |
} |
||
| 65 | |||
| 66 |
// Manual toggle use of Tellor testnet feed |
||
| 67 |
function toggleUseFallback() external returns (bool) {
|
||
| 68 |
_useFallback = !_useFallback; |
||
| 69 |
return _useFallback; |
||
| 70 |
} |
||
| 71 | |||
| 72 |
function setFallbackCaller(address _fallbackCaller) external requiresAuth {
|
||
| 73 |
address oldFallbackCaller = address(fallbackCaller); |
||
| 74 |
fallbackCaller = IFallbackCaller(_fallbackCaller); |
||
| 75 |
emit FallbackCallerChanged(oldFallbackCaller, _fallbackCaller); |
||
| 76 |
} |
||
| 77 | |||
| 78 |
// --- Oracle response wrapper functions --- |
||
| 79 |
/* |
||
| 80 |
* "_getCurrentFallbackResponse" fetches stETH/BTC from the Fallback, and returns it as a |
||
| 81 |
* FallbackResponse struct. |
||
| 82 |
*/ |
||
| 83 |
function _getCurrentFallbackResponse() |
||
| 84 |
internal |
||
| 85 |
view |
||
| 86 |
returns (FallbackResponse memory fallbackResponse) |
||
| 87 |
{
|
||
| 88 |
uint256 stEthBtcValue; |
||
| 89 |
uint256 stEthBtcTimestamp; |
||
| 90 |
bool stEthBtcRetrieved; |
||
| 91 | |||
| 92 |
// Attempt to get the Fallback's stETH/BTC price |
||
| 93 |
try fallbackCaller.getFallbackResponse() returns ( |
||
| 94 |
uint256 answer, |
||
| 95 |
uint256 timestampRetrieved, |
||
| 96 |
bool success |
||
| 97 |
) {
|
||
| 98 |
fallbackResponse.answer = answer; |
||
| 99 |
fallbackResponse.timestamp = timestampRetrieved; |
||
| 100 |
fallbackResponse.success = success; |
||
| 101 |
} catch {
|
||
| 102 |
return (fallbackResponse); |
||
| 103 |
} |
||
| 104 |
return (fallbackResponse); |
||
| 105 |
} |
||
| 106 |
} |
||
| 107 |
| Lines covered: | 1 / 1 (100.0%) |
|---|
| 1 |
// SPDX-License-Identifier: Unlicense |
||
| 2 |
pragma solidity ^0.8.0; |
||
| 3 | |||
| 4 |
interface IHevm {
|
||
| 5 |
// Set block.timestamp to newTimestamp |
||
| 6 |
function warp(uint256 newTimestamp) external; |
||
| 7 | |||
| 8 |
// Set block.number to newNumber |
||
| 9 |
function roll(uint256 newNumber) external; |
||
| 10 | |||
| 11 |
// Loads a storage slot from an address |
||
| 12 |
function load(address where, bytes32 slot) external returns (bytes32); |
||
| 13 | |||
| 14 |
// Stores a value to an address' storage slot |
||
| 15 |
function store(address where, bytes32 slot, bytes32 value) external; |
||
| 16 | |||
| 17 |
// Signs data (privateKey, digest) => (r, v, s) |
||
| 18 |
function sign(uint256 privateKey, bytes32 digest) external returns (uint8 r, bytes32 v, bytes32 s); |
||
| 19 | |||
| 20 |
// Gets address for a given private key |
||
| 21 |
function addr(uint256 privateKey) external returns (address addr); |
||
| 22 | |||
| 23 |
// Performs a foreign function call via terminal |
||
| 24 |
function ffi(string[] calldata inputs) external returns (bytes memory result); |
||
| 25 |
|
||
| 26 |
// Performs the next smart contract call with specified `msg.sender` |
||
| 27 |
function prank(address newSender) external; |
||
| 28 |
} |
||
| 29 | |||
| 30 |
√
|
⟳
|
IHevm constant hevm = IHevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); |
| Lines covered: | 3 / 3 (100.0%) |
|---|
| 1 |
pragma solidity ^0.8.0; |
||
| 2 | |||
| 3 |
abstract contract PropertiesConstants {
|
||
| 4 |
// Constant echidna addresses |
||
| 5 |
√
|
address constant USER1 = address(0x10000); |
|
| 6 |
√
|
address constant USER2 = address(0x20000); |
|
| 7 |
√
|
address constant USER3 = address(0x30000); |
|
| 8 |
uint256 constant INITIAL_BALANCE = 1000e18; |
||
| 9 |
} |
||
| 10 |
| Lines covered: | 36 / 57 (63.2%) |
|---|
| 1 |
pragma solidity ^0.8.0; |
||
| 2 | |||
| 3 |
abstract contract PropertiesAsserts {
|
||
| 4 |
event LogUint256(string,uint256); |
||
| 5 |
event LogAddress(string, address); |
||
| 6 |
event LogString(string); |
||
| 7 | |||
| 8 |
event AssertFail(string); |
||
| 9 |
event AssertEqFail(string); |
||
| 10 |
event AssertNeqFail(string); |
||
| 11 |
event AssertGteFail(string); |
||
| 12 |
event AssertGtFail(string); |
||
| 13 |
event AssertLteFail(string); |
||
| 14 |
event AssertLtFail(string); |
||
| 15 | |||
| 16 |
function assertWithMsg(bool b, string memory reason) internal {
|
||
| 17 |
√
|
⟳
|
if(!b){
|
| 18 |
⟳
|
emit AssertFail(reason); |
|
| 19 |
⟳
|
assert(false); |
|
| 20 |
} |
||
| 21 |
} |
||
| 22 | |||
| 23 |
/// @notice asserts that a is equal to b. Violations are logged using reason. |
||
| 24 |
function assertEq(uint256 a, uint256 b, string memory reason) internal {
|
||
| 25 |
√
|
if(a != b){
|
|
| 26 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 27 |
string memory bStr = PropertiesLibString.toString(b); |
||
| 28 |
bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr,"!=",bStr,", reason: ", reason);
|
||
| 29 |
emit AssertEqFail(string(assertMsg)); |
||
| 30 |
assert(false); |
||
| 31 |
} |
||
| 32 |
} |
||
| 33 | |||
| 34 |
/// @notice int256 version of assertEq |
||
| 35 |
function assertEq(int256 a, int256 b, string memory reason) internal {
|
||
| 36 |
if(a != b){
|
||
| 37 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 38 |
string memory bStr = PropertiesLibString.toString(b); |
||
| 39 |
bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr,"!=",bStr,", reason: ", reason);
|
||
| 40 |
emit AssertEqFail(string(assertMsg)); |
||
| 41 |
assert(false); |
||
| 42 |
} |
||
| 43 |
} |
||
| 44 | |||
| 45 |
/// @notice asserts that a is not equal to b. Violations are logged using reason. |
||
| 46 |
function assertNeq(uint256 a, uint256 b, string memory reason) internal {
|
||
| 47 |
if(a == b){
|
||
| 48 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 49 |
string memory bStr = PropertiesLibString.toString(b); |
||
| 50 |
bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr,"==",bStr,", reason: ", reason);
|
||
| 51 |
emit AssertNeqFail(string(assertMsg)); |
||
| 52 |
assert(false); |
||
| 53 |
} |
||
| 54 |
} |
||
| 55 | |||
| 56 |
/// @notice int256 version of assertNeq |
||
| 57 |
function assertNeq(int256 a, int256 b, string memory reason) internal {
|
||
| 58 |
if(a == b){
|
||
| 59 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 60 |
string memory bStr = PropertiesLibString.toString(b); |
||
| 61 |
bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr,"==",bStr,", reason: ", reason);
|
||
| 62 |
emit AssertNeqFail(string(assertMsg)); |
||
| 63 |
assert(false); |
||
| 64 |
} |
||
| 65 |
} |
||
| 66 | |||
| 67 |
/// @notice asserts that a is greater than or equal to b. Violations are logged using reason. |
||
| 68 |
function assertGte(uint256 a, uint256 b, string memory reason) internal {
|
||
| 69 |
√
|
⟳
|
if(!(a >= b)) {
|
| 70 |
⟳
|
string memory aStr = PropertiesLibString.toString(a); |
|
| 71 |
⟳
|
string memory bStr = PropertiesLibString.toString(b); |
|
| 72 |
⟳
|
bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr,"<",bStr," failed, reason: ", reason);
|
|
| 73 |
⟳
|
emit AssertGteFail(string(assertMsg)); |
|
| 74 |
⟳
|
assert(false); |
|
| 75 |
} |
||
| 76 |
} |
||
| 77 | |||
| 78 |
/// @notice int256 version of assertGte |
||
| 79 |
function assertGte(int256 a, int256 b, string memory reason) internal {
|
||
| 80 |
if(!(a >= b)) {
|
||
| 81 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 82 |
string memory bStr = PropertiesLibString.toString(b); |
||
| 83 |
bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr,"<",bStr," failed, reason: ", reason);
|
||
| 84 |
emit AssertGteFail(string(assertMsg)); |
||
| 85 |
assert(false); |
||
| 86 |
} |
||
| 87 |
} |
||
| 88 | |||
| 89 |
/// @notice asserts that a is greater than b. Violations are logged using reason. |
||
| 90 |
function assertGt(uint256 a, uint256 b, string memory reason) internal {
|
||
| 91 |
√
|
⟳
|
if(!(a > b)) {
|
| 92 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 93 |
string memory bStr = PropertiesLibString.toString(b); |
||
| 94 |
bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr,"<=", bStr," failed, reason: ", reason);
|
||
| 95 |
emit AssertGtFail(string(assertMsg)); |
||
| 96 |
assert(false); |
||
| 97 |
} |
||
| 98 |
} |
||
| 99 | |||
| 100 |
/// @notice int256 version of assertGt |
||
| 101 |
function assertGt(int256 a, int256 b, string memory reason) internal {
|
||
| 102 |
if(!(a > b)) {
|
||
| 103 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 104 |
string memory bStr = PropertiesLibString.toString(b); |
||
| 105 |
bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr,"<=", bStr," failed, reason: ", reason);
|
||
| 106 |
emit AssertGtFail(string(assertMsg)); |
||
| 107 |
assert(false); |
||
| 108 |
} |
||
| 109 |
} |
||
| 110 | |||
| 111 |
/// @notice asserts that a is less than or equal to b. Violations are logged using reason. |
||
| 112 |
function assertLte(uint256 a, uint256 b, string memory reason) internal {
|
||
| 113 |
√
|
if(!(a <= b)) {
|
|
| 114 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 115 |
string memory bStr = PropertiesLibString.toString(b); |
||
| 116 |
bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr,">", bStr," failed, reason: ", reason);
|
||
| 117 |
emit AssertLteFail(string(assertMsg)); |
||
| 118 |
assert(false); |
||
| 119 |
} |
||
| 120 |
} |
||
| 121 | |||
| 122 |
/// @notice int256 version of assertLte |
||
| 123 |
function assertLte(int256 a, int256 b, string memory reason) internal {
|
||
| 124 |
if(!(a <= b)) {
|
||
| 125 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 126 |
string memory bStr = PropertiesLibString.toString(b); |
||
| 127 |
bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr,">", bStr," failed, reason: ", reason);
|
||
| 128 |
emit AssertLteFail(string(assertMsg)); |
||
| 129 |
assert(false); |
||
| 130 |
} |
||
| 131 |
} |
||
| 132 | |||
| 133 |
/// @notice asserts that a is less than b. Violations are logged using reason. |
||
| 134 |
function assertLt(uint256 a, uint256 b, string memory reason) internal {
|
||
| 135 |
√
|
if(!(a < b)) {
|
|
| 136 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 137 |
string memory bStr = PropertiesLibString.toString(b); |
||
| 138 |
bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr,">=",bStr," failed, reason: ", reason);
|
||
| 139 |
emit AssertLtFail(string(assertMsg)); |
||
| 140 |
assert(false); |
||
| 141 |
} |
||
| 142 |
} |
||
| 143 | |||
| 144 |
/// @notice int256 version of assertLt |
||
| 145 |
function assertLt(int256 a, int256 b, string memory reason) internal {
|
||
| 146 |
if(!(a < b)) {
|
||
| 147 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 148 |
string memory bStr = PropertiesLibString.toString(b); |
||
| 149 |
bytes memory assertMsg = abi.encodePacked("Invalid: ", aStr,">=",bStr," failed, reason: ", reason);
|
||
| 150 |
emit AssertLtFail(string(assertMsg)); |
||
| 151 |
assert(false); |
||
| 152 |
} |
||
| 153 |
} |
||
| 154 | |||
| 155 |
/// @notice Clamps value to be between low and high, both inclusive |
||
| 156 |
√
|
⟳
|
function clampBetween(uint256 value, uint256 low, uint256 high) internal returns (uint256) {
|
| 157 |
√
|
⟳
|
if(value < low || value > high) {
|
| 158 |
√
|
⟳
|
uint ans = low + (value % (high - low + 1)); |
| 159 |
√
|
⟳
|
string memory valueStr = PropertiesLibString.toString(value); |
| 160 |
√
|
⟳
|
string memory ansStr = PropertiesLibString.toString(ans); |
| 161 |
√
|
⟳
|
bytes memory message = abi.encodePacked("Clamping value ", valueStr, " to ", ansStr);
|
| 162 |
√
|
⟳
|
emit LogString(string(message)); |
| 163 |
√
|
⟳
|
return ans; |
| 164 |
} |
||
| 165 |
√
|
⟳
|
return value; |
| 166 |
} |
||
| 167 | |||
| 168 |
/// @notice int256 version of clampBetween |
||
| 169 |
function clampBetween(int256 value, int256 low, int256 high) internal returns (int256) {
|
||
| 170 |
if(value < low || value > high) {
|
||
| 171 |
int range = high - low + 1; |
||
| 172 |
int clamped = (value - low) % (range); |
||
| 173 |
if (clamped < 0) clamped += range; |
||
| 174 |
int ans = low + clamped; |
||
| 175 |
string memory valueStr = PropertiesLibString.toString(value); |
||
| 176 |
string memory ansStr = PropertiesLibString.toString(ans); |
||
| 177 |
bytes memory message = abi.encodePacked("Clamping value ", valueStr, " to ", ansStr);
|
||
| 178 |
emit LogString(string(message)); |
||
| 179 |
return ans; |
||
| 180 |
} |
||
| 181 |
return value; |
||
| 182 |
} |
||
| 183 | |||
| 184 |
/// @notice clamps a to be less than b |
||
| 185 |
function clampLt(uint256 a, uint256 b) internal returns (uint256){
|
||
| 186 |
if ( !(a < b)) {
|
||
| 187 |
assertNeq(b, 0, "clampLt cannot clamp value a to be less than zero. Check your inputs/assumptions."); |
||
| 188 |
uint256 value = a % b; |
||
| 189 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 190 |
string memory valueStr = PropertiesLibString.toString(value); |
||
| 191 |
bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr);
|
||
| 192 |
emit LogString(string(message)); |
||
| 193 |
return value; |
||
| 194 |
} |
||
| 195 |
return a; |
||
| 196 |
} |
||
| 197 | |||
| 198 |
/// @notice int256 version of clampLt |
||
| 199 |
function clampLt(int256 a, int256 b) internal returns (int256){
|
||
| 200 |
if ( !(a < b)) {
|
||
| 201 |
int256 value = b-1; |
||
| 202 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 203 |
string memory valueStr = PropertiesLibString.toString(value); |
||
| 204 |
bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr);
|
||
| 205 |
emit LogString(string(message)); |
||
| 206 |
return value; |
||
| 207 |
} |
||
| 208 |
return a; |
||
| 209 |
} |
||
| 210 | |||
| 211 |
/// @notice clamps a to be less than or equal to b |
||
| 212 |
function clampLte(uint256 a, uint256 b) internal returns (uint256) {
|
||
| 213 |
if(!(a <= b)) {
|
||
| 214 |
uint256 value = a % (b+1); |
||
| 215 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 216 |
string memory valueStr = PropertiesLibString.toString(value); |
||
| 217 |
bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr);
|
||
| 218 |
emit LogString(string(message)); |
||
| 219 |
return value; |
||
| 220 |
} |
||
| 221 |
return a; |
||
| 222 |
} |
||
| 223 | |||
| 224 |
/// @notice int256 version of clampLte |
||
| 225 |
function clampLte(int256 a, int256 b) internal returns (int256) {
|
||
| 226 |
if(!(a <= b)) {
|
||
| 227 |
int256 value = b; |
||
| 228 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 229 |
string memory valueStr = PropertiesLibString.toString(value); |
||
| 230 |
bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr);
|
||
| 231 |
emit LogString(string(message)); |
||
| 232 |
return value; |
||
| 233 |
} |
||
| 234 |
return a; |
||
| 235 |
} |
||
| 236 | |||
| 237 |
/// @notice clamps a to be greater than b |
||
| 238 |
function clampGt(uint256 a, uint256 b) internal returns (uint256) {
|
||
| 239 |
if(!(a > b)){
|
||
| 240 |
assertNeq(b, type(uint256).max, "clampGt cannot clamp value a to be larger than uint256.max. Check your inputs/assumptions."); |
||
| 241 |
uint256 value = b+1; |
||
| 242 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 243 |
string memory valueStr = PropertiesLibString.toString(value); |
||
| 244 |
bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr);
|
||
| 245 |
emit LogString(string(message)); |
||
| 246 |
return value; |
||
| 247 |
} else {
|
||
| 248 |
return a; |
||
| 249 |
} |
||
| 250 |
} |
||
| 251 | |||
| 252 |
/// @notice int256 version of clampGt |
||
| 253 |
function clampGt(int256 a, int256 b) internal returns (int256) {
|
||
| 254 |
if(!(a > b)){
|
||
| 255 |
int256 value = b+1; |
||
| 256 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 257 |
string memory valueStr = PropertiesLibString.toString(value); |
||
| 258 |
bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr);
|
||
| 259 |
emit LogString(string(message)); |
||
| 260 |
return value; |
||
| 261 |
} else {
|
||
| 262 |
return a; |
||
| 263 |
} |
||
| 264 |
} |
||
| 265 | |||
| 266 |
/// @notice clamps a to be greater than or equal to b |
||
| 267 |
function clampGte(uint256 a, uint256 b) internal returns (uint256) {
|
||
| 268 |
if(!(a > b)){
|
||
| 269 |
uint256 value = b; |
||
| 270 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 271 |
string memory valueStr = PropertiesLibString.toString(value); |
||
| 272 |
bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr);
|
||
| 273 |
emit LogString(string(message)); |
||
| 274 |
return value; |
||
| 275 |
} |
||
| 276 |
return a; |
||
| 277 |
} |
||
| 278 | |||
| 279 |
/// @notice int256 version of clampGte |
||
| 280 |
function clampGte(int256 a, int256 b) internal returns (int256) {
|
||
| 281 |
if(!(a > b)){
|
||
| 282 |
int256 value = b; |
||
| 283 |
string memory aStr = PropertiesLibString.toString(a); |
||
| 284 |
string memory valueStr = PropertiesLibString.toString(value); |
||
| 285 |
bytes memory message = abi.encodePacked("Clamping value ", aStr, " to ", valueStr);
|
||
| 286 |
emit LogString(string(message)); |
||
| 287 |
return value; |
||
| 288 |
} |
||
| 289 |
return a; |
||
| 290 |
} |
||
| 291 |
} |
||
| 292 | |||
| 293 |
/// @notice Efficient library for creating string representations of integers. |
||
| 294 |
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/LibString.sol) |
||
| 295 |
/// @author Modified from Solady (https://github.com/Vectorized/solady/blob/main/src/utils/LibString.sol) |
||
| 296 |
/// @dev Name of the library is modified to prevent collisions with contract-under-test uses of LibString |
||
| 297 |
library PropertiesLibString {
|
||
| 298 | |||
| 299 |
function toString(int256 value) internal pure returns (string memory str) {
|
||
| 300 |
uint256 absValue = value >= 0 ? uint256(value) : uint256(-value); |
||
| 301 |
str = toString(absValue); |
||
| 302 | |||
| 303 |
if(value < 0) {
|
||
| 304 |
str = string(abi.encodePacked("-", str));
|
||
| 305 |
} |
||
| 306 |
} |
||
| 307 | |||
| 308 |
√
|
⟳
|
function toString(uint256 value) internal pure returns (string memory str) {
|
| 309 |
/// @solidity memory-safe-assembly |
||
| 310 |
assembly {
|
||
| 311 |
// The maximum value of a uint256 contains 78 digits (1 byte per digit), but we allocate 160 bytes |
||
| 312 |
// to keep the free memory pointer word aligned. We'll need 1 word for the length, 1 word for the |
||
| 313 |
// trailing zeros padding, and 3 other words for a max of 78 digits. In total: 5 * 32 = 160 bytes. |
||
| 314 |
√
|
⟳
|
let newFreeMemoryPointer := add(mload(0x40), 160) |
| 315 | |||
| 316 |
// Update the free memory pointer to avoid overriding our string. |
||
| 317 |
√
|
⟳
|
mstore(0x40, newFreeMemoryPointer) |
| 318 | |||
| 319 |
// Assign str to the end of the zone of newly allocated memory. |
||
| 320 |
√
|
⟳
|
str := sub(newFreeMemoryPointer, 32) |
| 321 | |||
| 322 |
// Clean the last word of memory it may not be overwritten. |
||
| 323 |
√
|
⟳
|
mstore(str, 0) |
| 324 | |||
| 325 |
// Cache the end of the memory to calculate the length later. |
||
| 326 |
√
|
⟳
|
let end := str |
| 327 | |||
| 328 |
// We write the string from rightmost digit to leftmost digit. |
||
| 329 |
// The following is essentially a do-while loop that also handles the zero case. |
||
| 330 |
// prettier-ignore |
||
| 331 |
√
|
⟳
|
for { let temp := value } 1 {} {
|
| 332 |
// Move the pointer 1 byte to the left. |
||
| 333 |
√
|
⟳
|
str := sub(str, 1) |
| 334 | |||
| 335 |
// Write the character to the pointer. |
||
| 336 |
// The ASCII index of the '0' character is 48. |
||
| 337 |
√
|
⟳
|
mstore8(str, add(48, mod(temp, 10))) |
| 338 | |||
| 339 |
// Keep dividing temp until zero. |
||
| 340 |
√
|
⟳
|
temp := div(temp, 10) |
| 341 | |||
| 342 |
// prettier-ignore |
||
| 343 |
√
|
⟳
|
if iszero(temp) { break }
|
| 344 |
} |
||
| 345 | |||
| 346 |
// Compute and cache the final total length of the string. |
||
| 347 |
√
|
⟳
|
let length := sub(end, str) |
| 348 | |||
| 349 |
// Move the pointer 32 bytes leftwards to make room for the length. |
||
| 350 |
√
|
⟳
|
str := sub(str, 32) |
| 351 | |||
| 352 |
// Store the string's length at the start of memory allocated for our string. |
||
| 353 |
√
|
⟳
|
mstore(str, length) |
| 354 |
} |
||
| 355 |
} |
||
| 356 | |||
| 357 |
function toString(address value) internal pure returns (string memory str){
|
||
| 358 |
bytes memory s = new bytes(40); |
||
| 359 |
for (uint i = 0; i < 20; i++) {
|
||
| 360 |
bytes1 b = bytes1(uint8(uint(uint160(value)) / (2**(8*(19 - i))))); |
||
| 361 |
bytes1 hi = bytes1(uint8(b) / 16); |
||
| 362 |
bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); |
||
| 363 |
s[2*i] = char(hi); |
||
| 364 |
s[2*i+1] = char(lo); |
||
| 365 |
} |
||
| 366 |
return string(s); |
||
| 367 |
} |
||
| 368 | |||
| 369 |
function char(bytes1 b) internal pure returns (bytes1 c) {
|
||
| 370 |
if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); |
||
| 371 |
else return bytes1(uint8(b) + 0x57); |
||
| 372 |
} |
||
| 373 |
} |
||
| 374 |